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:
Spencer Olson 2023-03-09 15:34:24 -06:00
parent cee5a86ca2
commit aad2d0c1e4
11 changed files with 1265 additions and 549 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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]) }

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }