fixup assignment bucket filtering
The assignments API index and users_index endpoints allow for a 'bucket' param to be passed that filters assignments based on certain criteria. This commit fixes inconsitencies with bucket filtering and makes bucket filtering predictable for teachers, admins, and observers. Teachers & Admins - An assignment is included in a 'bucket' if at least one assigned student meets the bucket criteria, e.g. for the 'past' bucket, an assignment will be returned if any assigned student has that assignment due in the past for them. Observers - An asignment is included in a 'bucket' if at least one assigned observed student meets the bucket criteria, .e.g. for the 'past' bucket, an assignment will be returned if any assigned observed student has that assignment due in the past for them. closes EVAL-2894 flag=none [fsc-timeout=60] [fsc-max-nodes=20] Test Plan: For an admin, teacher, observer, and student, verify the `bucket` param passed to the api/v1/courses/:id/assignments endpoint behaves in the following ways: For the steps below, replace APPLICABLE_STUDENTS with: - "at least one assigned student" if the current_user is a teacher or admin - "at least one assigned observed student" if the current_user is an observer - "themselves" if the current_user is a student `past` bucket - assignment is included if the assignment is due in the past for APPLICABLE_STUDENTS. `overdue` bucket - assignment is included if the assignment is due in the past, expects a submission, has not been submitted or graded, and is able to be submitted to by APPLICABLE_STUDENTS. `undated` bucket - assignment is included if the assignment is due without a due date for APPLICABLE_STUDENTS. `ungraded` bucket - assignment is included if the assignment expects a submission, the student has turned in work but has not been graded for APPLICABLE_STUDENTS. `unsubmitted` bucket - assignment is included if the assignment expects a submission and the student has not turned in work for APPLICABLE_STUDENTS. `upcoming` bucket - assignment is included if the assignment has a due date that is due in the next 10 days for APPLICABLE_STUDENTS. `future` bucket - assignment is included if the assignment is due without a due date or is due in the future for APPLICABLE_STUDENTS. Change-Id: I3255674cd32373bca36943030c35eaa9d15055b5 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/312938 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com> Reviewed-by: Jen Smith <jen.smith@instructure.com> QA-Review: Kai Bjorkman <kbjorkman@instructure.com> Product-Review: Cameron Ray <cameron.ray@instructure.com>
This commit is contained in:
parent
cee5a86ca2
commit
aad2d0c1e4
|
@ -1327,23 +1327,22 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def get_upcoming_assignments(course)
|
||||
assignments = AssignmentGroup.visible_assignments(
|
||||
visible_assignments = AssignmentGroup.visible_assignments(
|
||||
@current_user,
|
||||
course,
|
||||
course.assignment_groups.active
|
||||
).to_a
|
||||
)
|
||||
|
||||
log_course(course)
|
||||
|
||||
assignments.map! { |a| a.overridden_for(@current_user) }
|
||||
sorted = SortsAssignments.by_due_date({
|
||||
assignments: assignments,
|
||||
user: @current_user,
|
||||
session: session,
|
||||
upcoming_limit: 1.week.from_now
|
||||
})
|
||||
|
||||
sorted.upcoming.call.sort
|
||||
sorter = SortsAssignments.new(
|
||||
assignments_scope: visible_assignments,
|
||||
user: @current_user,
|
||||
session: session,
|
||||
course: course
|
||||
)
|
||||
sorter.assignments(:upcoming) do |assignments|
|
||||
assignments.group("assignments.id").order("MIN(submissions.cached_due_date) ASC").to_a
|
||||
end
|
||||
end
|
||||
|
||||
def log_course(course)
|
||||
|
|
|
@ -833,13 +833,14 @@ class AssignmentsApiController < ApplicationController
|
|||
include_params = Array(params[:include])
|
||||
|
||||
if params[:bucket]
|
||||
return invalid_bucket_error unless SortsAssignments::VALID_BUCKETS.include?(params[:bucket].to_sym)
|
||||
|
||||
users = current_user_and_observed(
|
||||
include_observed: include_params.include?("observed_users")
|
||||
)
|
||||
submissions_for_user = scope.with_submissions_for_user(users).flat_map(&:submissions)
|
||||
scope = SortsAssignments.bucket_filter(scope, params[:bucket], session, user, @current_user, @context, submissions_for_user)
|
||||
args = { assignments_scope: scope, user: @current_user, session: session, course: @context }
|
||||
args[:requested_user] = user if @current_user != user
|
||||
sorter = SortsAssignments.new(**args)
|
||||
begin
|
||||
scope = sorter.assignments(params[:bucket].to_sym)
|
||||
rescue SortsAssignments::InvalidBucketError
|
||||
return invalid_bucket_error
|
||||
end
|
||||
end
|
||||
|
||||
scope = scope.where(post_to_sis: value_to_boolean(params[:post_to_sis])) if params[:post_to_sis]
|
||||
|
@ -877,7 +878,7 @@ class AssignmentsApiController < ApplicationController
|
|||
return render json: { message: "Invalid assignment_ids: #{invalid_ids.join(",")}" }, status: :bad_request
|
||||
end
|
||||
|
||||
submissions = submissions_hash(include_params, assignments, submissions_for_user)
|
||||
submissions = submissions_hash(include_params, assignments)
|
||||
|
||||
include_all_dates = include_params.include?("all_dates")
|
||||
include_override_objects = include_params.include?("overrides") && @context.grants_any_right?(user, *RoleOverride::GRANULAR_MANAGE_ASSIGNMENT_PERMISSIONS)
|
||||
|
|
|
@ -3082,8 +3082,8 @@ class Assignment < ActiveRecord::Base
|
|||
chain.preload(:context)
|
||||
}
|
||||
|
||||
scope :expecting_submission, lambda {
|
||||
where.not(submission_types: [nil, ""] + %w[none not_graded on_paper wiki_page])
|
||||
scope :expecting_submission, lambda { |additional_excludes: []|
|
||||
where.not(submission_types: [nil, ""] + Array(additional_excludes) + %w[none not_graded on_paper wiki_page])
|
||||
}
|
||||
|
||||
scope :gradeable, -> { where.not(submission_types: %w[not_graded wiki_page]) }
|
||||
|
|
|
@ -2064,6 +2064,7 @@ class Submission < ActiveRecord::Base
|
|||
scope :with_assignment, -> { joins(:assignment).merge(Assignment.active) }
|
||||
|
||||
scope :graded, -> { where("(submissions.score IS NOT NULL AND submissions.workflow_state = 'graded') or submissions.excused = true") }
|
||||
scope :not_submitted_or_graded, -> { where(submission_type: nil).where("(submissions.score IS NULL OR submissions.workflow_state <> 'graded') AND submissions.excused IS NOT TRUE") }
|
||||
|
||||
scope :ungraded, -> { where(grade: nil).preload(:assignment) }
|
||||
|
||||
|
|
|
@ -890,22 +890,14 @@ module Api::V1::Assignment
|
|||
vericite_settings.to_unsafe_h
|
||||
end
|
||||
|
||||
def submissions_hash(include_params, assignments, submissions_for_user = nil)
|
||||
def submissions_hash(include_params, assignments)
|
||||
return {} unless include_params.include?("submission")
|
||||
|
||||
has_observed_users = include_params.include?("observed_users")
|
||||
|
||||
subs_list = if submissions_for_user
|
||||
assignment_ids = assignments.to_set(&:id)
|
||||
submissions_for_user.select do |s|
|
||||
assignment_ids.include?(s.assignment_id)
|
||||
end
|
||||
else
|
||||
users = current_user_and_observed(include_observed: has_observed_users)
|
||||
@context.submissions
|
||||
.where(assignment_id: assignments.map(&:id))
|
||||
.for_user(users)
|
||||
end
|
||||
users = current_user_and_observed(include_observed: has_observed_users)
|
||||
subs_list = @context.submissions
|
||||
.where(assignment_id: assignments.map(&:id))
|
||||
.for_user(users)
|
||||
|
||||
if has_observed_users
|
||||
# assignment id -> array. even if <2 results for a given
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2012 - present Instructure, Inc.
|
||||
# Copyright (C) 2023 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
|
@ -18,131 +18,101 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class SortsAssignments
|
||||
class InvalidBucketError < StandardError; end
|
||||
|
||||
VALID_BUCKETS = %i[past overdue undated ungraded unsubmitted upcoming future].freeze
|
||||
AssignmentsSortedByDueDate = Struct.new(*VALID_BUCKETS)
|
||||
|
||||
class << self
|
||||
def by_due_date(opts)
|
||||
assignments = opts.fetch(:assignments)
|
||||
user = opts.fetch(:user)
|
||||
current_user = opts[:current_user] || opts.fetch(:user)
|
||||
session = opts.fetch(:session)
|
||||
submissions = opts[:submissions]
|
||||
upcoming_limit = opts[:upcoming_limit] || 1.week.from_now
|
||||
course = opts[:course]
|
||||
def initialize(assignments_scope:, user:, session:, course:, requested_user: nil)
|
||||
@assignments_scope = assignments_scope
|
||||
@user = user
|
||||
@session = session
|
||||
@course = course
|
||||
@requested_user = requested_user
|
||||
end
|
||||
|
||||
AssignmentsSortedByDueDate.new(
|
||||
-> { past(assignments) },
|
||||
-> { overdue(assignments, user, session, submissions) },
|
||||
-> { undated(assignments) },
|
||||
-> { ungraded_for_user_and_session(assignments, user, current_user, session) },
|
||||
-> { unsubmitted_for_user_and_session(course, assignments, user, current_user, session) },
|
||||
-> { upcoming(assignments, upcoming_limit) },
|
||||
-> { future(assignments) }
|
||||
)
|
||||
end
|
||||
def assignments(bucket, &block)
|
||||
raise InvalidBucketError if VALID_BUCKETS.exclude?(bucket)
|
||||
|
||||
def past(assignments)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |assignment| assignment.due_at < Time.now }
|
||||
end
|
||||
@now = Time.zone.now
|
||||
filter(bucket, &block)
|
||||
end
|
||||
|
||||
def dated(assignments)
|
||||
assignments ||= []
|
||||
assignments.reject { |assignment| assignment.due_at.nil? }
|
||||
end
|
||||
private
|
||||
|
||||
def undated(assignments)
|
||||
assignments ||= []
|
||||
assignments.select { |assignment| assignment.due_at.nil? }
|
||||
end
|
||||
|
||||
def unsubmitted_for_user_and_session(course, assignments, user, current_user, session)
|
||||
return [] unless course.grants_right?(current_user, session, :manage_grades)
|
||||
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? &&
|
||||
assignment.submission_for_student(user)[:id].blank?
|
||||
end
|
||||
end
|
||||
|
||||
def upcoming(assignments, limit = 1.week.from_now)
|
||||
assignments ||= []
|
||||
dated(assignments).select { |a| due_between?(a, Time.now, limit) }
|
||||
end
|
||||
|
||||
def future(assignments)
|
||||
assignments - past(assignments)
|
||||
end
|
||||
|
||||
def up_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at < time }
|
||||
end
|
||||
|
||||
def down_to(assignments, time)
|
||||
dated(assignments).select { |assignment| assignment.due_at > time }
|
||||
end
|
||||
|
||||
def ungraded_for_user_and_session(assignments, user, current_user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.grants_right?(current_user, session, :grade) &&
|
||||
assignment.expects_submission? &&
|
||||
Assignments::NeedsGradingCountQuery.new(assignment, user).count > 0
|
||||
end
|
||||
end
|
||||
|
||||
def without_graded_submission(assignments, submissions)
|
||||
assignments ||= []
|
||||
submissions ||= []
|
||||
submissions_by_assignment = submissions.index_by(&:assignment_id)
|
||||
assignments.select do |assignment|
|
||||
match = submissions_by_assignment[assignment.id]
|
||||
!match || match.without_graded_submission?
|
||||
end
|
||||
end
|
||||
|
||||
def user_allowed_to_submit(assignments, user, session)
|
||||
assignments ||= []
|
||||
assignments.select do |assignment|
|
||||
assignment.expects_submission? && assignment.grants_right?(user, session, :submit)
|
||||
end
|
||||
end
|
||||
|
||||
def overdue(assignments, user, session, submissions)
|
||||
submissions ||= []
|
||||
assignments = past(assignments)
|
||||
user_allowed_to_submit(assignments, user, session) &
|
||||
without_graded_submission(assignments, submissions)
|
||||
end
|
||||
|
||||
def bucket_filter(given_scope, bucket, session, user, current_user, context, submissions_for_user)
|
||||
overridden_assignments = given_scope.map { |a| a.overridden_for(user) }
|
||||
|
||||
observed_students = ObserverEnrollment.observed_students(context, user)
|
||||
user_for_sorting = if observed_students.count == 1
|
||||
observed_students.keys.first
|
||||
else
|
||||
user
|
||||
end
|
||||
|
||||
sorted_assignments = by_due_date(
|
||||
course: context,
|
||||
assignments: overridden_assignments,
|
||||
user: user_for_sorting,
|
||||
current_user: current_user,
|
||||
session: session,
|
||||
submissions: submissions_for_user
|
||||
)
|
||||
filtered_assignment_ids = sorted_assignments.send(bucket).call.map(&:id)
|
||||
given_scope.where(id: filtered_assignment_ids)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def due_between?(assignment, start_time, end_time)
|
||||
assignment.due_at >= start_time && assignment.due_at <= end_time
|
||||
def filter(bucket)
|
||||
assignments_in_bucket = buckets.fetch(bucket).call(assignments_for_students)
|
||||
if block_given?
|
||||
yield assignments_in_bucket
|
||||
else
|
||||
@assignments_scope.where(id: assignments_in_bucket)
|
||||
end
|
||||
end
|
||||
|
||||
def buckets
|
||||
{
|
||||
past: filters[:has_date] >> filters[:past_due],
|
||||
overdue: (
|
||||
filters[:has_date] >> filters[:past_due] >>
|
||||
filters[:expects_submission].call(additional_excludes: %w[external_tool online_quiz attendance]) >>
|
||||
filters[:not_submitted_or_graded] >> filters[:can_submit]
|
||||
),
|
||||
undated: filters[:has_no_date],
|
||||
ungraded: filters[:expects_submission].call >> filters[:needs_grading],
|
||||
unsubmitted: filters[:expects_submission].call >> filters[:not_submitted_or_graded],
|
||||
upcoming: filters[:has_date] >> filters[:due_soon],
|
||||
future: filters[:due_in_future]
|
||||
}
|
||||
end
|
||||
|
||||
def filters
|
||||
@filters ||= {
|
||||
has_no_date: ->(scope) { scope.where(submissions: { cached_due_date: nil }) },
|
||||
has_date: ->(scope) { scope.where.not(submissions: { cached_due_date: nil }) },
|
||||
past_due: ->(scope) { scope.where("submissions.cached_due_date < ?", @now) },
|
||||
due_in_future: ->(scope) { scope.where("submissions.cached_due_date IS NULL OR submissions.cached_due_date >= ?", @now) },
|
||||
due_soon: ->(scope) { scope.where("submissions.cached_due_date >= ? AND submissions.cached_due_date <= ?", @now, 1.week.from_now(@now)) },
|
||||
expects_submission: lambda do |additional_excludes: ["external_tool"]|
|
||||
->(scope) { scope.expecting_submission(additional_excludes: additional_excludes) }
|
||||
end,
|
||||
needs_grading: ->(scope) { scope.where(Submission.needs_grading_conditions) },
|
||||
not_submitted_or_graded: ->(scope) { scope.merge(Submission.not_submitted_or_graded) },
|
||||
can_submit: lambda do |scope|
|
||||
students_by_id = students.index_by(&:id)
|
||||
students_by_assignment_id = scope.pluck("submissions.assignment_id", "submissions.user_id").each_with_object({}) do |(assignment_id, user_id), acc|
|
||||
acc[assignment_id] ||= []
|
||||
acc[assignment_id] << students_by_id[user_id]
|
||||
end
|
||||
|
||||
assignments = @course.assignments.except(:order).where(id: students_by_assignment_id.keys).select do |assignment|
|
||||
submittable_by_any_student?(assignment, students_by_assignment_id[assignment.id])
|
||||
end
|
||||
|
||||
@course.assignments.where(id: assignments).except(:order)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def submittable_by_any_student?(assignment, students)
|
||||
students.any? { |student| student.present? && assignment.grants_right?(student, :submit) }
|
||||
end
|
||||
|
||||
def assignments_for_students
|
||||
@course.assignments.where(id: assignment_ids).except(:order).joins(:submissions).where(submissions: { user: students })
|
||||
end
|
||||
|
||||
def assignment_ids
|
||||
@assignment_ids ||= @assignments_scope.pluck(:id)
|
||||
end
|
||||
|
||||
def students
|
||||
@students ||= if @requested_user.present? && @user != @requested_user
|
||||
[@requested_user]
|
||||
elsif @course.grants_right?(@user, @session, :read_as_admin)
|
||||
@course.students_visible_to(@user).merge(Enrollment.of_student_type).distinct.to_a
|
||||
elsif @course.observers.where(id: @user).exists?
|
||||
ObserverEnrollment.observed_students(@course, @user).keys
|
||||
else
|
||||
[@user]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -608,6 +608,7 @@ describe AssignmentsApiController, type: :request do
|
|||
|
||||
describe "assignment bucketing" do
|
||||
before :once do
|
||||
@now = Time.zone.now
|
||||
course_with_student(active_all: true)
|
||||
@student1 = @user
|
||||
@section = @course.course_sections.create!(name: "test section")
|
||||
|
@ -619,17 +620,17 @@ describe AssignmentsApiController, type: :request do
|
|||
student_in_section(@section2, user: @student2)
|
||||
|
||||
# names based on student 1's due dates
|
||||
@past_assignment = @course.assignments.create!(title: "past", only_visible_to_overrides: true, due_at: (Time.now - 10.days))
|
||||
create_section_override_for_assignment(@past_assignment, { course_section: @section, due_at: (Time.now - 10.days) })
|
||||
@past_assignment = @course.assignments.create!(title: "past", only_visible_to_overrides: true, due_at: 10.days.ago(@now))
|
||||
create_section_override_for_assignment(@past_assignment, { course_section: @section, due_at: 10.days.ago(@now) })
|
||||
|
||||
@overdue_assignment = @course.assignments.create!(title: "overdue", only_visible_to_overrides: true, submission_types: "online")
|
||||
create_section_override_for_assignment(@overdue_assignment, { course_section: @section, due_at: (Time.now - 10.days) })
|
||||
create_section_override_for_assignment(@overdue_assignment, { course_section: @section, due_at: 10.days.ago(@now) })
|
||||
|
||||
@far_future_assignment = @course.assignments.create!(title: "far future", only_visible_to_overrides: true)
|
||||
create_section_override_for_assignment(@far_future_assignment, { course_section: @section, due_at: (Time.now + 30.days) })
|
||||
create_section_override_for_assignment(@far_future_assignment, { course_section: @section, due_at: 30.days.from_now(@now) })
|
||||
|
||||
@upcoming_assignment = @course.assignments.create!(title: "upcoming", only_visible_to_overrides: true)
|
||||
create_section_override_for_assignment(@upcoming_assignment, { course_section: @section, due_at: (Time.now + 1.day) })
|
||||
create_section_override_for_assignment(@upcoming_assignment, { course_section: @section, due_at: 1.day.from_now(@now) })
|
||||
|
||||
@undated_assignment = @course.assignments.create!(title: "undated", only_visible_to_overrides: true)
|
||||
override = create_section_override_for_assignment(@undated_assignment, { course_section: @section, due_at: nil })
|
||||
|
@ -637,8 +638,8 @@ describe AssignmentsApiController, type: :request do
|
|||
override.save
|
||||
|
||||
# student2 overrides
|
||||
create_section_override_for_assignment(@past_assignment, { course_section: @section2, due_at: (Time.now - 10.days) })
|
||||
create_section_override_for_assignment(@far_future_assignment, { course_section: @section2, due_at: (Time.now - 10.days) })
|
||||
create_section_override_for_assignment(@past_assignment, { course_section: @section2, due_at: 10.days.ago(@now) })
|
||||
create_section_override_for_assignment(@far_future_assignment, { course_section: @section2, due_at: 10.days.ago(@now) })
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -659,14 +660,14 @@ describe AssignmentsApiController, type: :request do
|
|||
expect(json["errors"]["bucket"].first["message"]).to eq "bucket name must be one of the following: past, overdue, undated, ungraded, unsubmitted, upcoming, future"
|
||||
end
|
||||
|
||||
def assignment_index_bucketed_api_call(bucket)
|
||||
def assignment_index_bucketed_api_call(bucket, opts = {})
|
||||
api_call(:get,
|
||||
"/api/v1/courses/#{@course.id}/assignments.json",
|
||||
{ controller: "assignments_api",
|
||||
action: "index",
|
||||
format: "json",
|
||||
course_id: @course.id.to_s,
|
||||
bucket: bucket })
|
||||
bucket: bucket }.merge(opts))
|
||||
end
|
||||
|
||||
def assert_call_gets_assignments(bucket, assignments)
|
||||
|
@ -706,16 +707,38 @@ describe AssignmentsApiController, type: :request do
|
|||
end
|
||||
|
||||
context "as a teacher" do
|
||||
it "uses default assignment dates" do
|
||||
teacher = @course.teachers.first
|
||||
user_session(teacher)
|
||||
@user = teacher
|
||||
before do
|
||||
@teacher = @course.teachers.first
|
||||
user_session(@teacher)
|
||||
@user = @teacher
|
||||
end
|
||||
|
||||
it "includes assignments in buckets if any assigned students meet the criteria" do
|
||||
assert_calls_get_assignments(
|
||||
past: [@past_assignment],
|
||||
undated: [@upcoming_assignment, @undated_assignment, @overdue_assignment, @far_future_assignment]
|
||||
past: [@past_assignment, @overdue_assignment, @far_future_assignment],
|
||||
undated: [@undated_assignment]
|
||||
)
|
||||
end
|
||||
|
||||
it "supports sorting bucketed assignments by name" do
|
||||
@course.assignments.create!(title: "z", due_at: 2.days.from_now(@now))
|
||||
@course.assignments.create!(title: "a", due_at: 3.days.from_now(@now))
|
||||
assignments_json = assignment_index_bucketed_api_call(:upcoming, order_by: :name)
|
||||
expect(assignments_json.pluck("name")).to eq %w[a upcoming z]
|
||||
end
|
||||
|
||||
it "supports sorting bucketed assignments by latest due date, ascending" do
|
||||
z_assignment = @course.assignments.create!(title: "z")
|
||||
create_adhoc_override_for_assignment(z_assignment, @student1, due_at: 1.hour.from_now(@now))
|
||||
create_adhoc_override_for_assignment(z_assignment, @student2, due_at: 2.hours.from_now(@now))
|
||||
|
||||
a_assignment = @course.assignments.create!(title: "a")
|
||||
create_adhoc_override_for_assignment(a_assignment, @student1, due_at: 1.hour.ago(@now))
|
||||
create_adhoc_override_for_assignment(a_assignment, @student2, due_at: 2.days.from_now(@now))
|
||||
|
||||
assignments_json = assignment_index_bucketed_api_call(:upcoming, order_by: :due_at)
|
||||
expect(assignments_json.pluck("name")).to eq %w[z upcoming a]
|
||||
end
|
||||
end
|
||||
|
||||
context "as an observer" do
|
||||
|
@ -741,7 +764,7 @@ describe AssignmentsApiController, type: :request do
|
|||
)
|
||||
end
|
||||
|
||||
it "treats multi-student observers like course observers" do
|
||||
it "includes assignments in buckets if any observed students meet the criteria" do
|
||||
@observer_enrollment = @course.enroll_user(@observer, "ObserverEnrollment", section: @section, enrollment_state: "active", allow_multiple_enrollments: true)
|
||||
@observer_enrollment.update_attribute(:associated_user_id, @student1.id)
|
||||
@observer_enrollment = @course.enroll_user(@observer, "ObserverEnrollment", section: @section, enrollment_state: "active", allow_multiple_enrollments: true)
|
||||
|
@ -750,21 +773,9 @@ describe AssignmentsApiController, type: :request do
|
|||
assert_calls_get_assignments(
|
||||
future: [@upcoming_assignment, @far_future_assignment, @undated_assignment],
|
||||
upcoming: [@upcoming_assignment],
|
||||
past: [@past_assignment, @overdue_assignment],
|
||||
past: [@past_assignment, @overdue_assignment, @far_future_assignment],
|
||||
undated: [@undated_assignment],
|
||||
overdue: []
|
||||
)
|
||||
end
|
||||
|
||||
it "uses sections dates when observing a whole course" do
|
||||
@observer_enrollment = @course.enroll_user(@observer, "ObserverEnrollment", section: @section, enrollment_state: "active")
|
||||
|
||||
assert_calls_get_assignments(
|
||||
future: [@upcoming_assignment, @far_future_assignment, @undated_assignment],
|
||||
upcoming: [@upcoming_assignment],
|
||||
past: [@past_assignment, @overdue_assignment],
|
||||
undated: [@undated_assignment],
|
||||
overdue: []
|
||||
overdue: [@overdue_assignment]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1249,117 +1249,160 @@ describe CoursesController do
|
|||
@c2 = @s2.add_comment(author: @teacher, comment: "some comment2")
|
||||
end
|
||||
|
||||
before do
|
||||
user_session(@me)
|
||||
context "as a teacher" do
|
||||
before do
|
||||
@course1.update!(default_view: "assignments")
|
||||
student_in_course(active_all: true, course: @course1)
|
||||
@assignment = @course1.assignments.create!(due_at: 1.day.from_now)
|
||||
user_session(@teacher)
|
||||
end
|
||||
|
||||
it "shows unpublished upcoming assignments" do
|
||||
@assignment.unpublish
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments)).to include @assignment
|
||||
end
|
||||
|
||||
it "does not show duplicate upcoming assignments" do
|
||||
create_adhoc_override_for_assignment(@assignment, @me, due_at: 2.days.from_now)
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments).count).to eq 1
|
||||
end
|
||||
|
||||
it "includes assignments where at least one assigned student has the assignment upcoming" do
|
||||
create_adhoc_override_for_assignment(@assignment, @me, due_at: 1.day.ago)
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments)).to include @assignment
|
||||
end
|
||||
|
||||
it "excludes assignments where no assigned students have the assignment upcoming" do
|
||||
@assignment.update!(only_visible_to_overrides: true)
|
||||
create_adhoc_override_for_assignment(@assignment, @me, due_at: 1.day.ago)
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments)).not_to include @assignment
|
||||
end
|
||||
|
||||
it "sorts assignments by their earliest upcoming due date, ascending" do
|
||||
create_adhoc_override_for_assignment(@assignment, @me, due_at: 3.days.from_now)
|
||||
later_assignment = @course1.assignments.create!(due_at: 2.days.from_now)
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments)).to eq [@assignment, later_assignment]
|
||||
end
|
||||
end
|
||||
|
||||
it "works for module view" do
|
||||
@course1.default_view = "modules"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
context "as a student" do
|
||||
before do
|
||||
user_session(@me)
|
||||
end
|
||||
|
||||
it "works for assignments view" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
it "works for module view" do
|
||||
@course1.default_view = "modules"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
|
||||
it "disables management and set env urls on assignment homepage" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:URLS][:new_assignment_url]).not_to be_nil
|
||||
expect(controller.js_env[:PERMISSIONS][:manage]).to be_falsey
|
||||
end
|
||||
it "works for assignments view" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
|
||||
it "sets ping_url" do
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:ping_url]).not_to be_nil
|
||||
end
|
||||
it "disables management and set env urls on assignment homepage" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:URLS][:new_assignment_url]).not_to be_nil
|
||||
expect(controller.js_env[:PERMISSIONS][:manage]).to be_falsey
|
||||
end
|
||||
|
||||
it "does not show unpublished assignments to students" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
@a1a = @course1.assignments.new(title: "some assignment course 1", due_at: 1.day.from_now)
|
||||
@a1a.save
|
||||
@a1a.unpublish
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments).map(&:id).include?(@a1a.id)).to be_falsey
|
||||
end
|
||||
it "sets ping_url" do
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:ping_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "works for wiki view" do
|
||||
@course1.default_view = "wiki"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
it "does not show unpublished assignments to students" do
|
||||
@course1.default_view = "assignments"
|
||||
@course1.save!
|
||||
@a1a = @course1.assignments.new(title: "some assignment course 1", due_at: 1.day.from_now)
|
||||
@a1a.save
|
||||
@a1a.unpublish
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:upcoming_assignments).map(&:id).include?(@a1a.id)).to be_falsey
|
||||
end
|
||||
|
||||
it "works for wiki view with draft state enabled" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:WIKI_RIGHTS].symbolize_keys).to eql({ read: true })
|
||||
expect(controller.js_env[:PAGE_RIGHTS].symbolize_keys).to eql({ read: true })
|
||||
expect(controller.js_env[:COURSE_TITLE]).to eql @course1.name
|
||||
end
|
||||
it "works for wiki view" do
|
||||
@course1.default_view = "wiki"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
|
||||
it "works for wiki view with home page announcements enabled" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.show_announcements_on_home_page = true
|
||||
@course1.home_page_announcement_limit = 3
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:COURSE_HOME]).to be_truthy
|
||||
expect(controller.js_env[:SHOW_ANNOUNCEMENTS]).to be_truthy
|
||||
expect(controller.js_env[:ANNOUNCEMENT_LIMIT]).to eq(3)
|
||||
end
|
||||
it "works for wiki view with draft state enabled" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:WIKI_RIGHTS].symbolize_keys).to eql({ read: true })
|
||||
expect(controller.js_env[:PAGE_RIGHTS].symbolize_keys).to eql({ read: true })
|
||||
expect(controller.js_env[:COURSE_TITLE]).to eql @course1.name
|
||||
end
|
||||
|
||||
it "does not show announcements for public users" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.show_announcements_on_home_page = true
|
||||
@course1.home_page_announcement_limit = 3
|
||||
@course1.is_public = true
|
||||
@course1.save!
|
||||
remove_user_session
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(response).to be_successful
|
||||
expect(controller.js_env[:COURSE_HOME]).to be_truthy
|
||||
expect(controller.js_env[:SHOW_ANNOUNCEMENTS]).to be_falsey
|
||||
end
|
||||
it "works for wiki view with home page announcements enabled" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.show_announcements_on_home_page = true
|
||||
@course1.home_page_announcement_limit = 3
|
||||
@course1.save!
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(controller.js_env[:COURSE_HOME]).to be_truthy
|
||||
expect(controller.js_env[:SHOW_ANNOUNCEMENTS]).to be_truthy
|
||||
expect(controller.js_env[:ANNOUNCEMENT_LIMIT]).to eq(3)
|
||||
end
|
||||
|
||||
it "works for syllabus view" do
|
||||
@course1.default_view = "syllabus"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
it "does not show announcements for public users" do
|
||||
@course1.wiki_pages.create!(title: "blah").set_as_front_page!
|
||||
@course1.reload
|
||||
@course1.default_view = "wiki"
|
||||
@course1.show_announcements_on_home_page = true
|
||||
@course1.home_page_announcement_limit = 3
|
||||
@course1.is_public = true
|
||||
@course1.save!
|
||||
remove_user_session
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(response).to be_successful
|
||||
expect(controller.js_env[:COURSE_HOME]).to be_truthy
|
||||
expect(controller.js_env[:SHOW_ANNOUNCEMENTS]).to be_falsey
|
||||
end
|
||||
|
||||
it "works for feed view" do
|
||||
@course1.default_view = "feed"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
it "works for syllabus view" do
|
||||
@course1.default_view = "syllabus"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
|
||||
it "only shows recent feedback if user is student in specified course" do
|
||||
course_with_teacher(active_all: true, user: @student)
|
||||
@course3 = @course
|
||||
get "show", params: { id: @course3.id }
|
||||
expect(assigns(:show_recent_feedback)).to be_falsey
|
||||
it "works for feed view" do
|
||||
@course1.default_view = "feed"
|
||||
@course1.save
|
||||
get "show", params: { id: @course1.id }
|
||||
expect(assigns(:recent_feedback).count).to eq 1
|
||||
expect(assigns(:recent_feedback).first.assignment_id).to eq @a1.id
|
||||
end
|
||||
|
||||
it "only shows recent feedback if user is student in specified course" do
|
||||
course_with_teacher(active_all: true, user: @student)
|
||||
@course3 = @course
|
||||
get "show", params: { id: @course3.id }
|
||||
expect(assigns(:show_recent_feedback)).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -635,6 +635,43 @@ describe Assignment do
|
|||
end
|
||||
end
|
||||
|
||||
describe "scope: expecting_submission" do
|
||||
it "includes assignments expecting online submissions" do
|
||||
assignment_model(submission_types: "online_text_entry,online_url,online_upload", course: @course)
|
||||
expect(Assignment.expecting_submission).not_to be_empty
|
||||
end
|
||||
|
||||
it "includes submissions for assignments expecting external_tool submissions" do
|
||||
assignment_model(submission_types: "external_tool", course: @course)
|
||||
expect(Assignment.expecting_submission).not_to be_empty
|
||||
end
|
||||
|
||||
it "optionally excludes other assignment types" do
|
||||
assignment_model(submission_types: "external_tool", course: @course)
|
||||
expect(Assignment.expecting_submission(additional_excludes: "external_tool")).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submissions for assignments expecting on_paper submissions" do
|
||||
assignment_model(submission_types: "on_paper", course: @course)
|
||||
expect(Assignment.expecting_submission).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submissions for assignments expecting wiki_page submissions" do
|
||||
assignment_model(submission_types: "wiki_page", course: @course)
|
||||
expect(Assignment.expecting_submission).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submissions for assignments not expecting submissions" do
|
||||
assignment_model(submission_types: "none", course: @course)
|
||||
expect(Assignment.expecting_submission).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submissions for not graded assignments" do
|
||||
assignment_model(submission_types: "not_graded", course: @course)
|
||||
expect(Assignment.expecting_submission).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "#visible_to_students_in_course_with_da" do
|
||||
let(:student_enrollment) { @course.enrollments.find_by(user: @student) }
|
||||
let(:visible_assignments) do
|
||||
|
|
|
@ -4066,6 +4066,32 @@ describe Submission do
|
|||
end
|
||||
end
|
||||
|
||||
describe "scope: not_submitted_or_graded" do
|
||||
before do
|
||||
@assignment = @course.assignments.create!(submission_types: "online_text_entry")
|
||||
@submission = @assignment.submissions.find_by(user: @student)
|
||||
end
|
||||
|
||||
it "includes submissions where the student has not submitted and has not been graded" do
|
||||
expect(Submission.not_submitted_or_graded).to include @submission
|
||||
end
|
||||
|
||||
it "excludes submissions where the student has submitted" do
|
||||
@assignment.submit_homework(@student, body: "hi")
|
||||
expect(Submission.not_submitted_or_graded).not_to include @submission
|
||||
end
|
||||
|
||||
it "excludes submissions where the student has been graded" do
|
||||
@assignment.grade_student(@student, grader: @teacher, grade: 10)
|
||||
expect(Submission.not_submitted_or_graded).not_to include @submission
|
||||
end
|
||||
|
||||
it "excludes excused submissions" do
|
||||
@assignment.grade_student(@student, grader: @teacher, excused: true)
|
||||
expect(Submission.not_submitted_or_graded).not_to include @submission
|
||||
end
|
||||
end
|
||||
|
||||
describe "scope: postable" do
|
||||
subject(:submissions) { assignment.submissions.postable }
|
||||
|
||||
|
|
Loading…
Reference in New Issue