add 'extended' late_policy_status
flag=extended_submission_state [fsc-max-nodes=15] [fsc-timeout=45] refs PFS-19811 Change-Id: Ia151a868913e6cd1d3998e0602595509b45b16d9 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/290850 Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Petra Ashment <pashment@instructure.com> Product-Review: Jody Sailor Reviewed-by: Spencer Olson <solson@instructure.com> Reviewed-by: Kai Bjorkman <kbjorkman@instructure.com>
This commit is contained in:
parent
703b5d88a6
commit
947fc1b84a
|
@ -318,6 +318,7 @@ class ApplicationController < ActionController::Base
|
|||
product_tours files_dnd usage_rights_discussion_topics
|
||||
granular_permissions_manage_users create_course_subaccount_picker
|
||||
lti_deep_linking_module_index_menu_modal lti_multiple_assignment_deep_linking buttons_and_icons_root_account
|
||||
extended_submission_state
|
||||
].freeze
|
||||
JS_ENV_BRAND_ACCOUNT_FEATURES = [
|
||||
:embedded_release_notes
|
||||
|
|
|
@ -63,7 +63,7 @@ class GradebookSettingsController < ApplicationController
|
|||
:sort_rows_by_setting_key,
|
||||
:sort_rows_by_direction,
|
||||
:view_ungraded_as_zero,
|
||||
{ colors: %i[late missing resubmitted dropped excused] }
|
||||
{ colors: %i[late missing resubmitted dropped excused extended] }
|
||||
)
|
||||
gradebook_settings_params[:enter_grades_as] = params[:gradebook_settings][:enter_grades_as]
|
||||
gradebook_settings_params.permit!
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
# "type": "boolean"
|
||||
# },
|
||||
# "late_policy_status": {
|
||||
# "description": "The status of the submission in relation to the late policy. Can be late, missing, none, or null.",
|
||||
# "description": "The status of the submission in relation to the late policy. Can be late, missing, extended, none, or null.",
|
||||
# "example": "missing",
|
||||
# "type": "string"
|
||||
# },
|
||||
|
@ -767,7 +767,8 @@ class SubmissionsApiController < ApplicationController
|
|||
# Sets the "excused" status of an assignment.
|
||||
#
|
||||
# @argument submission[late_policy_status] [String]
|
||||
# Sets the late policy status to either "late", "missing", "none", or null.
|
||||
# Sets the late policy status to either "late", "missing", "extended", "none", or null.
|
||||
# NB: "extended" values can only be set in the UI when the "UI features for 'extended' Submissions" Account Feature is on
|
||||
#
|
||||
# @argument submission[seconds_late_override] [Integer]
|
||||
# Sets the seconds late if late policy status is "late"
|
||||
|
@ -1044,7 +1045,8 @@ class SubmissionsApiController < ApplicationController
|
|||
# Sets the "excused" status of an assignment.
|
||||
#
|
||||
# @argument submission[late_policy_status] [String]
|
||||
# Sets the late policy status to either "late", "missing", "none", or null.
|
||||
# Sets the late policy status to either "late", "missing", "extended", "none", or null.
|
||||
# NB: "extended" values can only be set in the UI when the "UI features for 'extended' Submissions" Account Feature is on
|
||||
#
|
||||
# @argument submission[seconds_late_override] [Integer]
|
||||
# Sets the seconds late if late policy status is "late"
|
||||
|
|
|
@ -91,6 +91,7 @@ module Interfaces::SubmissionInterface
|
|||
graphql_name "LatePolicyStatusType"
|
||||
value "late"
|
||||
value "missing"
|
||||
value "extended"
|
||||
value "none"
|
||||
end
|
||||
|
||||
|
|
|
@ -3922,7 +3922,7 @@ class Course < ActiveRecord::Base
|
|||
# Who..." for unsubmitted.
|
||||
expire_time = Setting.get("late_policy_tainted_submissions", 1.hour).to_i
|
||||
Rails.cache.fetch(["late_policy_tainted_submissions", self].cache_key, expires_in: expire_time) do
|
||||
submissions.except(:order).where(late_policy_status: %w[missing late none]).exists?
|
||||
submissions.except(:order).where(late_policy_status: %w[missing late extended none]).exists?
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -173,7 +173,7 @@ module SpeedGrader
|
|||
|
||||
word_count_enabled = assignment.root_account.feature_enabled?(:word_count_in_speed_grader)
|
||||
res[:submissions] = submissions.map do |sub|
|
||||
submission_methods = %i[submission_history late external_tool_url entered_score entered_grade seconds_late missing]
|
||||
submission_methods = %i[submission_history late external_tool_url entered_score entered_grade seconds_late missing late_policy_status]
|
||||
submission_methods << :word_count if word_count_enabled
|
||||
json = sub.as_json(
|
||||
include_root: false,
|
||||
|
@ -226,7 +226,7 @@ module SpeedGrader
|
|||
json["submission_history"] = json["submission_history"].map do |version|
|
||||
# to avoid a call to the DB in Submission#missing?
|
||||
version.assignment = sub.assignment
|
||||
version_methods = %i[versioned_attachments late missing external_tool_url]
|
||||
version_methods = %i[versioned_attachments late missing external_tool_url late_policy_status]
|
||||
version_methods << :word_count if word_count_enabled
|
||||
version.as_json(only: submission_json_fields, methods: version_methods).tap do |version_json|
|
||||
version_json["submission"]["has_originality_report"] = version.has_originality_report?
|
||||
|
|
|
@ -137,7 +137,7 @@ class Submission < ActiveRecord::Base
|
|||
validates :points_deducted, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :seconds_late_override, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :extra_attempts, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true
|
||||
validates :late_policy_status, inclusion: %w[none missing late], allow_nil: true
|
||||
validates :late_policy_status, inclusion: %w[none missing late extended], allow_nil: true
|
||||
validates :cached_tardiness, inclusion: ["missing", "late"], allow_nil: true
|
||||
validate :ensure_grader_can_grade
|
||||
validate :extra_attempts_can_only_be_set_on_online_uploads
|
||||
|
@ -186,7 +186,9 @@ class Submission < ActiveRecord::Base
|
|||
joins(:assignment)
|
||||
.where(<<~SQL.squish)
|
||||
/* excused submissions cannot be missing */
|
||||
excused IS NOT TRUE AND NOT (
|
||||
excused IS NOT TRUE
|
||||
AND (late_policy_status IS DISTINCT FROM 'extended')
|
||||
AND NOT (
|
||||
/* teacher said it's missing, 'nuff said. */
|
||||
/* we're doing a double 'NOT' here to avoid 'ORs' that could slow down the query */
|
||||
late_policy_status IS DISTINCT FROM 'missing' AND NOT
|
||||
|
@ -213,28 +215,29 @@ class Submission < ActiveRecord::Base
|
|||
}
|
||||
|
||||
scope :late, lambda {
|
||||
left_joins(:quiz_submission)
|
||||
.where("
|
||||
submissions.excused IS NOT TRUE AND (
|
||||
left_joins(:quiz_submission).where(<<~SQL.squish)
|
||||
submissions.excused IS NOT TRUE
|
||||
AND (
|
||||
submissions.late_policy_status = 'late' OR
|
||||
(submissions.late_policy_status IS NULL AND submissions.submitted_at >= submissions.cached_due_date +
|
||||
CASE submissions.submission_type WHEN 'online_quiz' THEN interval '1 minute' ELSE interval '0 minutes' END
|
||||
AND (submissions.quiz_submission_id IS NULL OR quiz_submissions.workflow_state = 'complete'))
|
||||
)
|
||||
")
|
||||
SQL
|
||||
}
|
||||
|
||||
scope :not_late, lambda {
|
||||
left_joins(:quiz_submission)
|
||||
.where("
|
||||
submissions.excused IS TRUE OR (
|
||||
left_joins(:quiz_submission).where(<<~SQL.squish)
|
||||
submissions.excused IS TRUE
|
||||
OR (late_policy_status IS NOT DISTINCT FROM 'extended')
|
||||
OR (
|
||||
submissions.late_policy_status is distinct from 'late' AND
|
||||
(submissions.submitted_at IS NULL OR submissions.cached_due_date IS NULL OR
|
||||
submissions.submitted_at < submissions.cached_due_date +
|
||||
CASE submissions.submission_type WHEN 'online_quiz' THEN interval '1 minute' ELSE interval '0 minutes' END
|
||||
OR quiz_submissions.workflow_state <> 'complete')
|
||||
)
|
||||
")
|
||||
SQL
|
||||
}
|
||||
|
||||
GradedAtBookmarker = BookmarkedCollection::SimpleBookmarker.new(Submission, :graded_at)
|
||||
|
@ -1561,7 +1564,7 @@ class Submission < ActiveRecord::Base
|
|||
def late_policy_status_manually_applied?
|
||||
cleared_late = late_policy_status_was == "late" && ["none", nil].include?(late_policy_status)
|
||||
cleared_none = late_policy_status_was == "none" && late_policy_status.nil?
|
||||
late_policy_status == "missing" || late_policy_status == "late" || cleared_late || cleared_none
|
||||
late_policy_status == "missing" || late_policy_status == "late" || late_policy_status == "extended" || cleared_late || cleared_none
|
||||
end
|
||||
private :late_policy_status_manually_applied?
|
||||
|
||||
|
@ -2416,6 +2419,14 @@ class Submission < ActiveRecord::Base
|
|||
end
|
||||
alias_method :missing, :missing?
|
||||
|
||||
def extended?
|
||||
return false if excused?
|
||||
return late_policy_status == "extended" if late_policy_status.present?
|
||||
|
||||
false
|
||||
end
|
||||
alias_method :extended, :extended?
|
||||
|
||||
def graded?
|
||||
excused || (!!score && workflow_state == "graded")
|
||||
end
|
||||
|
|
|
@ -148,6 +148,7 @@ class GradeSummaryAssignmentPresenter
|
|||
classes << "assignment_graded" if graded?
|
||||
classes << special_class
|
||||
classes << "excused" if excused?
|
||||
classes << "extended" if extended?
|
||||
classes.join(" ")
|
||||
end
|
||||
|
||||
|
@ -163,6 +164,10 @@ class GradeSummaryAssignmentPresenter
|
|||
submission.try(:excused?)
|
||||
end
|
||||
|
||||
def extended?
|
||||
submission.try(:extended?)
|
||||
end
|
||||
|
||||
def deduction_present?
|
||||
!!(submission&.points_deducted&.> 0)
|
||||
end
|
||||
|
|
|
@ -411,9 +411,16 @@ $icon-size: 1.4rem;
|
|||
font-size: 1.2em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submission-status-pill {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.submission-late-pill,
|
||||
.submission-missing-pill,
|
||||
.submission-excused-pill {
|
||||
.submission-excused-pill,
|
||||
.submission-extended-pill {
|
||||
display: inline;
|
||||
font-size: 1rem;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
|
|
|
@ -119,7 +119,7 @@ select.grading_box.pass_fail {
|
|||
width: inherit;
|
||||
}
|
||||
|
||||
span.late-pill, span.missing-pill {
|
||||
span.late-pill, span.missing-pill, span.extended-pill {
|
||||
padding-#{direction(left)}: 12px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
|
|
@ -188,6 +188,8 @@
|
|||
<span class="submission-missing-pill"></span>
|
||||
<% elsif assignment_presenter.late? %>
|
||||
<span class="submission-late-pill"></span>
|
||||
<% elsif assignment_presenter.extended? %>
|
||||
<span class="submission-extended-pill"></span>
|
||||
<% end %>
|
||||
</td>
|
||||
|
||||
|
|
|
@ -208,6 +208,8 @@
|
|||
<span class="submission-late-pill"></span>
|
||||
<% elsif @submission.missing? %>
|
||||
<span class="submission-missing-pill"></span>
|
||||
<% elsif @submission.extended? %>
|
||||
<span class="submission-extended-pill"></span>
|
||||
<% end %>
|
||||
<% if @assignment.allowed_attempts&.> 0 %>
|
||||
<div class="submission-details-header__attempts_info">
|
||||
|
|
|
@ -599,13 +599,13 @@
|
|||
{
|
||||
"warning_type": "Mass Assignment",
|
||||
"warning_code": 70,
|
||||
"fingerprint": "58ed5eb1b2c3a696a4c3d567b80b70a85f2feb5dadd4a202465778353c843211",
|
||||
"fingerprint": "5976651f66acf7863fe628cb40ae2a2ebb217d3319177e8609b0f2168f91582d",
|
||||
"check_name": "MassAssignment",
|
||||
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
|
||||
"file": "app/controllers/gradebook_settings_controller.rb",
|
||||
"line": 65,
|
||||
"line": 69,
|
||||
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
|
||||
"code": "params.require(:gradebook_settings).permit({ :filter_columns_by => ([:context_module_id, :grading_period_id, :assignment_group_id]), :filter_rows_by => ([:section_id, :student_group_id]), :selected_view_options_filters => ([]) }, :enter_grades_as, :show_concluded_enrollments, :show_inactive_enrollments, :show_unpublished_assignments, :student_column_display_as, :student_column_secondary_info, :sort_rows_by_column_id, :sort_rows_by_setting_key, :sort_rows_by_direction, :view_ungraded_as_zero, :colors => ([:late, :missing, :resubmitted, :dropped, :excused])).permit!",
|
||||
"code": "params.require(:gradebook_settings).permit({ :filter_columns_by => ([:context_module_id, :grading_period_id, :assignment_group_id, :submissions]), :filter_rows_by => ([:section_id, :student_group_id]), :selected_view_options_filters => ([]) }, :enter_grades_as, :hide_assignment_group_totals, :hide_total, :show_concluded_enrollments, :show_inactive_enrollments, :show_unpublished_assignments, :show_separate_first_last_names, :student_column_display_as, :student_column_secondary_info, :sort_rows_by_column_id, :sort_rows_by_setting_key, :sort_rows_by_direction, :view_ungraded_as_zero, :colors => ([:late, :missing, :resubmitted, :dropped, :excused, :extended])).permit!",
|
||||
"render_path": null,
|
||||
"location": {
|
||||
"type": "method",
|
||||
|
@ -1658,6 +1658,6 @@
|
|||
"note": ""
|
||||
}
|
||||
],
|
||||
"updated": "2022-02-16 09:17:28 -0600",
|
||||
"brakeman_version": "5.2.1"
|
||||
"updated": "2022-05-19 11:52:58 -0600",
|
||||
"brakeman_version": "5.2.3"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
extended_submission_state:
|
||||
state: hidden
|
||||
applies_to: RootAccount
|
||||
display_name: UI features for "extended" Submissions
|
||||
description: Enables UI features for managing "extended" Submissions.
|
||||
environments:
|
||||
ci:
|
||||
state: allowed_on
|
||||
development:
|
||||
state: allowed_on
|
|
@ -139,7 +139,10 @@ require('jest-fetch-mock').enableFetchMocks()
|
|||
|
||||
window.scroll = () => {}
|
||||
window.ENV = {
|
||||
use_rce_enhancements: true
|
||||
use_rce_enhancements: true,
|
||||
FEATURES: {
|
||||
extended_submission_state: true
|
||||
}
|
||||
}
|
||||
|
||||
Enzyme.configure({adapter: new Adapter()})
|
||||
|
|
|
@ -23,6 +23,7 @@ const PILL_MAPPING = {
|
|||
late: () => ({id: 'late', text: formatMessage('Late'), variant: 'danger'}),
|
||||
graded: () => ({id: 'graded', text: formatMessage('Graded')}),
|
||||
excused: () => ({id: 'excused', text: formatMessage('Excused')}),
|
||||
extended: () => ({id: 'extended', text: formatMessage('Extended')}),
|
||||
submitted: () => ({id: 'submitted', text: formatMessage('Submitted')}),
|
||||
new_grades: () => ({id: 'new_grades', text: formatMessage('Graded')}),
|
||||
new_feedback: () => ({id: 'new_feedback', text: formatMessage('Feedback')}),
|
||||
|
@ -109,6 +110,9 @@ export function getBadgesForItems(items) {
|
|||
} else if (items.some(showPillForOverdueStatus.bind(this, 'late'))) {
|
||||
badges.push(PILL_MAPPING.late())
|
||||
}
|
||||
if (items.some(i => i.status && i.status.extended)) {
|
||||
badges.push(PILL_MAPPING.extended())
|
||||
}
|
||||
if (items.some(i => i.status && i.newActivity && i.status.has_feedback)) {
|
||||
badges.push(PILL_MAPPING.new_feedback())
|
||||
}
|
||||
|
|
|
@ -41,11 +41,21 @@ function mountComponent(customProps) {
|
|||
}
|
||||
|
||||
QUnit.module('SubmissionTrayRadioInputGroup', {
|
||||
setup() {
|
||||
this.originalENV = window.ENV
|
||||
window.ENV = {
|
||||
FEATURES: {
|
||||
extended_submission_state: true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getRadioOption(value) {
|
||||
return this.wrapper.find(`input[type="radio"][value="${value}"]`).instance()
|
||||
},
|
||||
|
||||
teardown() {
|
||||
window.ENV = this.originalENV
|
||||
this.wrapper.unmount()
|
||||
}
|
||||
})
|
||||
|
@ -65,7 +75,7 @@ test('renders all SubmissionTrayRadioInputs enabled if disabled is false', funct
|
|||
const inputDisabledStatus = this.wrapper
|
||||
.find('SubmissionTrayRadioInput')
|
||||
.map(input => input.props().disabled)
|
||||
deepEqual(inputDisabledStatus, [false, false, false, false])
|
||||
deepEqual(inputDisabledStatus, [false, false, false, false, false])
|
||||
})
|
||||
|
||||
test('renders all SubmissionTrayRadioInputs disabled if disabled is false', function () {
|
||||
|
@ -73,7 +83,7 @@ test('renders all SubmissionTrayRadioInputs disabled if disabled is false', func
|
|||
const inputDisabledStatus = this.wrapper
|
||||
.find('SubmissionTrayRadioInput')
|
||||
.map(input => input.props().disabled)
|
||||
deepEqual(inputDisabledStatus, [true, true, true, true])
|
||||
deepEqual(inputDisabledStatus, [true, true, true, true, true])
|
||||
})
|
||||
|
||||
test('renders with "none" selected if the submission is not late, missing, or excused', function () {
|
||||
|
@ -149,6 +159,20 @@ test('renders with "Missing" selected if the submission is not excused and is mi
|
|||
strictEqual(radio.checked, true)
|
||||
})
|
||||
|
||||
test('renders with "Extended" selected if the submission is not excused and is extended', function () {
|
||||
this.wrapper = mountComponent({
|
||||
submission: {
|
||||
excused: false,
|
||||
late: false,
|
||||
missing: false,
|
||||
latePolicyStatus: 'extended',
|
||||
secondsLate: 0
|
||||
}
|
||||
})
|
||||
const radio = this.getRadioOption('extended')
|
||||
strictEqual(radio.checked, true)
|
||||
})
|
||||
|
||||
QUnit.module('SubmissionTrayRadioInputGroup#handleRadioInputChanged', suiteHooks => {
|
||||
let wrapper
|
||||
let updateSubmission
|
||||
|
|
|
@ -43,7 +43,7 @@ describe Submission do
|
|||
|
||||
it { is_expected.to validate_numericality_of(:points_deducted).is_greater_than_or_equal_to(0).allow_nil }
|
||||
it { is_expected.to validate_numericality_of(:seconds_late_override).is_greater_than_or_equal_to(0).allow_nil }
|
||||
it { is_expected.to validate_inclusion_of(:late_policy_status).in_array(%w[none missing late]).allow_nil }
|
||||
it { is_expected.to validate_inclusion_of(:late_policy_status).in_array(%w[none missing late extended]).allow_nil }
|
||||
it { is_expected.to validate_inclusion_of(:cached_tardiness).in_array(["missing", "late"]).allow_nil }
|
||||
|
||||
it { is_expected.to delegate_method(:auditable?).to(:assignment).with_prefix(true) }
|
||||
|
@ -1198,6 +1198,29 @@ describe Submission do
|
|||
end
|
||||
end
|
||||
|
||||
context "when change late_policy_status from late to extended" do
|
||||
before do
|
||||
@assignment.course.update!(late_policy: @late_policy)
|
||||
@assignment.submit_homework(@student, body: "a body")
|
||||
|
||||
submission.update!(
|
||||
score: 700,
|
||||
late_policy_status: "late",
|
||||
seconds_late_override: 4.hours
|
||||
)
|
||||
end
|
||||
|
||||
it "removes late penalty from score" do
|
||||
expect { submission.update!(late_policy_status: "extended") }
|
||||
.to change { submission.score }.from(300).to(700)
|
||||
end
|
||||
|
||||
it "sets points_deducted to nil" do
|
||||
expect { submission.update!(late_policy_status: "extended") }
|
||||
.to change { submission.points_deducted }.from(400).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context "when changing late_policy_status from none to nil" do
|
||||
before do
|
||||
@assignment.update!(due_at: 1.hour.from_now)
|
||||
|
@ -3831,6 +3854,12 @@ describe Submission do
|
|||
expect(Submission.missing).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submission when past due and extended" do
|
||||
@submission.update(late_policy_status: "extended")
|
||||
|
||||
expect(Submission.missing).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submission when past due and assignment does not expect a submission" do
|
||||
@submission.assignment.update(submission_types: "none")
|
||||
|
||||
|
@ -3896,6 +3925,12 @@ describe Submission do
|
|||
expect(Submission.missing).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submission when late_policy_status is extended" do
|
||||
@submission.update(late_policy_status: "extended")
|
||||
|
||||
expect(Submission.missing).to be_empty
|
||||
end
|
||||
|
||||
it "excludes submission when submitted before the due date" do
|
||||
@submission.update(submitted_at: 2.days.ago(@now))
|
||||
|
||||
|
@ -7099,6 +7134,10 @@ describe Submission do
|
|||
@late_quiz2_submission = @late_quiz2.submission
|
||||
Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
|
||||
|
||||
@late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
|
||||
@late_quiz_extended_submission = @late_quiz_extended.submission
|
||||
Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")
|
||||
|
||||
@timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
|
||||
@timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
|
||||
Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
|
||||
|
@ -7195,6 +7234,10 @@ describe Submission do
|
|||
expect(@late_submission_ids).to include(@ongoing_timely_quiz_marked_late_submission.id)
|
||||
end
|
||||
|
||||
it "excludes quizzes that are late but have been marked as extended" do
|
||||
expect(@late_submission_ids).not_to include(@late_quiz_extended_submission.id)
|
||||
end
|
||||
|
||||
### Homeworks
|
||||
it "excludes unsubmitted homeworks" do
|
||||
expect(@late_submission_ids).not_to include(@unsubmitted_hw.id)
|
||||
|
@ -7266,6 +7309,10 @@ describe Submission do
|
|||
@late_quiz2_submission = @late_quiz2.submission
|
||||
Submission.where(id: @late_quiz2_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour)
|
||||
|
||||
@late_quiz_extended = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
|
||||
@late_quiz_extended_submission = @late_quiz_extended.submission
|
||||
Submission.where(id: @late_quiz_extended_submission).update_all(submitted_at: @now, cached_due_date: @now - 1.hour, late_policy_status: "extended")
|
||||
|
||||
@timely_quiz_marked_late = generate_quiz_submission(@quiz, student: User.create, finished_at: @now)
|
||||
@timely_quiz_marked_late_submission = @timely_quiz_marked_late.submission
|
||||
Submission.where(id: @timely_quiz_marked_late_submission).update_all(submitted_at: @now, cached_due_date: nil)
|
||||
|
@ -7362,6 +7409,10 @@ describe Submission do
|
|||
expect(@not_late_submission_ids).not_to include(@ongoing_timely_quiz_marked_late_submission.id)
|
||||
end
|
||||
|
||||
it "includes quizzes that are late but have been marked as extended" do
|
||||
expect(@not_late_submission_ids).to include(@late_quiz_extended_submission.id)
|
||||
end
|
||||
|
||||
### Homeworks
|
||||
it "includes unsubmitted homeworks" do
|
||||
expect(@not_late_submission_ids).to include(@unsubmitted_hw.id)
|
||||
|
|
|
@ -3479,6 +3479,7 @@ class Gradebook extends React.Component<GradebookProps, GradebookState> {
|
|||
late: false,
|
||||
missing: false,
|
||||
excused: false,
|
||||
late_policy_status: null,
|
||||
seconds_late: 0
|
||||
}
|
||||
const submission = this.getSubmission(studentId, assignmentId) || fakeSubmission
|
||||
|
|
|
@ -171,6 +171,7 @@ export default class AssignmentCellFormatter {
|
|||
const submissionData = {
|
||||
dropped: submission.drop,
|
||||
excused: submission.excused,
|
||||
extended: submission.late_policy_status == 'extended',
|
||||
grade: assignment.grading_type === 'pass_fail' ? submission.rawGrade : submission.grade,
|
||||
late: submission.late,
|
||||
missing: submission.missing,
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
export function classNamesForAssignmentCell(assignment, submissionData) {
|
||||
const classNames = []
|
||||
const classNames: string[] = []
|
||||
|
||||
if (submissionData) {
|
||||
// Exclusive Classes (only one of these can be used at a time)
|
||||
|
@ -25,6 +25,8 @@ export function classNamesForAssignmentCell(assignment, submissionData) {
|
|||
classNames.push('dropped')
|
||||
} else if (submissionData.excused) {
|
||||
classNames.push('excused')
|
||||
} else if (submissionData.extended) {
|
||||
classNames.push('extended')
|
||||
} else if (submissionData.late) {
|
||||
classNames.push('late')
|
||||
} else if (submissionData.resubmitted) {
|
||||
|
|
|
@ -45,7 +45,8 @@ GridColor.propTypes = {
|
|||
missing: string,
|
||||
resubmitted: string,
|
||||
dropped: string,
|
||||
excused: string
|
||||
excused: string,
|
||||
extended: string
|
||||
}).isRequired,
|
||||
statuses: arrayOf(string)
|
||||
}
|
||||
|
|
|
@ -89,7 +89,8 @@ export default class SubmissionTray extends React.Component {
|
|||
colors: shape({
|
||||
late: string.isRequired,
|
||||
missing: string.isRequired,
|
||||
excused: string.isRequired
|
||||
excused: string.isRequired,
|
||||
extended: string.isRequired
|
||||
}).isRequired,
|
||||
onClose: func.isRequired,
|
||||
onGradeSubmission: func.isRequired,
|
||||
|
@ -114,6 +115,7 @@ export default class SubmissionTray extends React.Component {
|
|||
gradedAt: string.isRequired,
|
||||
late: bool.isRequired,
|
||||
missing: bool.isRequired,
|
||||
extended: bool.isRequired,
|
||||
pointsDeducted: number,
|
||||
postedAt: string.isRequired,
|
||||
secondsLate: number.isRequired,
|
||||
|
|
|
@ -35,6 +35,8 @@ function checkedValue(submission, assignment) {
|
|||
return 'missing'
|
||||
} else if (submission.late) {
|
||||
return 'late'
|
||||
} else if (submission.latePolicyStatus === 'extended') {
|
||||
return 'extended'
|
||||
}
|
||||
|
||||
return 'none'
|
||||
|
@ -73,7 +75,9 @@ export default class SubmissionTrayRadioInputGroup extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const radioOptions = ['none', 'late', 'missing', 'excused'].map(status => (
|
||||
const optionValues = ['none', 'late', 'missing', 'excused']
|
||||
if (ENV.FEATURES && ENV.FEATURES.extended_submission_state) optionValues.push('extended')
|
||||
const radioOptions = optionValues.map(status => (
|
||||
<SubmissionTrayRadioInput
|
||||
key={status}
|
||||
checked={checkedValue(this.props.submission, this.props.assignment) === status}
|
||||
|
@ -109,7 +113,8 @@ SubmissionTrayRadioInputGroup.propTypes = {
|
|||
colors: shape({
|
||||
late: string.isRequired,
|
||||
missing: string.isRequired,
|
||||
excused: string.isRequired
|
||||
excused: string.isRequired,
|
||||
extended: string.isRequired
|
||||
}).isRequired,
|
||||
disabled: bool.isRequired,
|
||||
latePolicy: shape({
|
||||
|
@ -120,7 +125,8 @@ SubmissionTrayRadioInputGroup.propTypes = {
|
|||
excused: bool.isRequired,
|
||||
late: bool.isRequired,
|
||||
missing: bool.isRequired,
|
||||
secondsLate: number.isRequired
|
||||
secondsLate: number.isRequired,
|
||||
latePolicyStatus: string.isRequired
|
||||
}).isRequired,
|
||||
submissionUpdating: bool.isRequired,
|
||||
updateSubmission: func.isRequired
|
||||
|
|
|
@ -52,7 +52,7 @@ describe('Statuses Modal', () => {
|
|||
expect(getByRole('heading', {name: /Statuses/i})).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders five StatusColorListItems', () => {
|
||||
it('renders six StatusColorListItems', () => {
|
||||
const onClose = jest.fn()
|
||||
const afterUpdateStatusColors = jest.fn()
|
||||
|
||||
|
@ -65,7 +65,7 @@ describe('Statuses Modal', () => {
|
|||
)
|
||||
|
||||
const {getAllByRole} = within(document.body)
|
||||
expect(getAllByRole('listitem').length).toBe(5)
|
||||
expect(getAllByRole('listitem').length).toBe(6)
|
||||
})
|
||||
|
||||
it('onClose is called when closed', () => {
|
||||
|
|
|
@ -21,6 +21,7 @@ import Color from 'tinycolor2'
|
|||
export type StatusColors = {
|
||||
dropped: string
|
||||
excused: string
|
||||
extended: string
|
||||
late: string
|
||||
missing: string
|
||||
resubmitted: string
|
||||
|
@ -42,6 +43,7 @@ export const defaultColors = {
|
|||
const defaultStatusColors = {
|
||||
dropped: defaultColors.orange,
|
||||
excused: defaultColors.yellow,
|
||||
extended: defaultColors.lavender,
|
||||
late: defaultColors.blue,
|
||||
missing: defaultColors.salmon,
|
||||
resubmitted: defaultColors.green
|
||||
|
|
|
@ -21,11 +21,13 @@ import {useScope as useI18nScope} from '@canvas/i18n'
|
|||
const I18n = useI18nScope('gradebook')
|
||||
|
||||
export const statuses = ['late', 'missing', 'resubmitted', 'dropped', 'excused']
|
||||
if (ENV.FEATURES && ENV.FEATURES.extended_submission_state) statuses.push('extended')
|
||||
|
||||
export const statusesTitleMap = {
|
||||
late: I18n.t('Late'),
|
||||
missing: I18n.t('Missing'),
|
||||
resubmitted: I18n.t('Resubmitted'),
|
||||
dropped: I18n.t('Dropped'),
|
||||
excused: I18n.t('Excused')
|
||||
excused: I18n.t('Excused'),
|
||||
extended: I18n.t('Extended')
|
||||
}
|
||||
|
|
|
@ -108,7 +108,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
)
|
||||
},
|
||||
|
||||
checkForCsvExport: function() {
|
||||
checkForCsvExport: function () {
|
||||
const currentProgress = get(window, 'ENV.GRADEBOOK_OPTIONS.gradebook_csv_progress')
|
||||
const attachment = get(window, 'ENV.GRADEBOOK_OPTIONS.attachment')
|
||||
|
||||
|
@ -150,7 +150,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
},
|
||||
|
||||
gradingPeriods: (function() {
|
||||
gradingPeriods: (function () {
|
||||
const periods = get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods')
|
||||
const deserializedPeriods = GradingPeriodsApi.deserializePeriods(periods)
|
||||
const optionForAllPeriods = {
|
||||
|
@ -172,7 +172,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
})(),
|
||||
|
||||
selectedGradingPeriod: function(key, newValue) {
|
||||
selectedGradingPeriod: function (key, newValue) {
|
||||
let savedGP
|
||||
const savedGradingPeriodId = userSettings.contextGet('gradebook_current_grading_period')
|
||||
if (savedGradingPeriodId) {
|
||||
|
@ -192,11 +192,11 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property(),
|
||||
|
||||
speedGraderUrl: function() {
|
||||
speedGraderUrl: function () {
|
||||
return `${contextUrl}/gradebook/speed_grader?assignment_id=${this.get('selectedAssignment.id')}`
|
||||
}.property('selectedAssignment'),
|
||||
|
||||
studentUrl: function() {
|
||||
studentUrl: function () {
|
||||
return `${contextUrl}/grades/${this.get('selectedStudent.id')}`
|
||||
}.property('selectedStudent'),
|
||||
|
||||
|
@ -213,7 +213,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
|
||||
hideOutcomes: (() => !get(window, 'ENV.GRADEBOOK_OPTIONS.outcome_gradebook_enabled')).property(),
|
||||
|
||||
showDownloadSubmissionsButton: function() {
|
||||
showDownloadSubmissionsButton: function () {
|
||||
const hasSubmittedSubmissions = this.get('selectedAssignment.has_submitted_submissions')
|
||||
const allowList = ['online_upload', 'online_text_entry', 'online_url']
|
||||
const submissionTypes = this.get('selectedAssignment.submission_types')
|
||||
|
@ -224,7 +224,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
|
||||
hideStudentNames: false,
|
||||
|
||||
showConcludedEnrollments: function() {
|
||||
showConcludedEnrollments: function () {
|
||||
if (!ENV.GRADEBOOK_OPTIONS.settings) {
|
||||
return false
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.property()
|
||||
.volatile(),
|
||||
|
||||
updateShowConcludedEnrollmentsSetting: function() {
|
||||
updateShowConcludedEnrollmentsSetting: function () {
|
||||
ajax.request({
|
||||
dataType: 'json',
|
||||
type: 'put',
|
||||
|
@ -248,7 +248,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
|
||||
finalGradeOverrideEnabled: (() => ENV.GRADEBOOK_OPTIONS.final_grade_override_enabled).property(),
|
||||
|
||||
allowFinalGradeOverride: function() {
|
||||
allowFinalGradeOverride: function () {
|
||||
if (!ENV.GRADEBOOK_OPTIONS.course_settings) {
|
||||
return false
|
||||
}
|
||||
|
@ -258,7 +258,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.property()
|
||||
.volatile(),
|
||||
|
||||
updateAllowFinalGradeOverride: function() {
|
||||
updateAllowFinalGradeOverride: function () {
|
||||
ajax.request({
|
||||
dataType: 'json',
|
||||
type: 'put',
|
||||
|
@ -269,7 +269,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
})
|
||||
}.observes('allowFinalGradeOverride'),
|
||||
|
||||
selectedStudentFinalGradeOverrideChanged: function() {
|
||||
selectedStudentFinalGradeOverrideChanged: function () {
|
||||
const student = this.get('selectedStudent')
|
||||
|
||||
if (!student) {
|
||||
|
@ -300,7 +300,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
)
|
||||
.on('init'),
|
||||
|
||||
selectedAssignmentPointsPossible: function() {
|
||||
selectedAssignmentPointsPossible: function () {
|
||||
return I18n.n(this.get('selectedAssignment.points_possible'))
|
||||
}.property('selectedAssignment'),
|
||||
|
||||
|
@ -329,10 +329,9 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
$('#gradebook-export').prop('disabled', true)
|
||||
$('#last-exported-gradebook').hide()
|
||||
|
||||
return $.ajaxJSON(
|
||||
ENV.GRADEBOOK_OPTIONS.export_gradebook_csv_url,
|
||||
'POST'
|
||||
).then(attachment_progress => this.pollGradebookCsvProgress(attachment_progress))
|
||||
return $.ajaxJSON(ENV.GRADEBOOK_OPTIONS.export_gradebook_csv_url, 'POST').then(
|
||||
attachment_progress => this.pollGradebookCsvProgress(attachment_progress)
|
||||
)
|
||||
},
|
||||
|
||||
gradeUpdated(submissions) {
|
||||
|
@ -449,19 +448,19 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
})
|
||||
},
|
||||
|
||||
hideStudentNamesChanged: function() {
|
||||
hideStudentNamesChanged: function () {
|
||||
this.set('ariaAnnounced', null)
|
||||
}.observes('hideStudentNames'),
|
||||
|
||||
setupSubmissionCallback: function() {
|
||||
setupSubmissionCallback: function () {
|
||||
Ember.$.subscribe('submissions_updated', this.updateSubmissionsFromExternal.bind(this))
|
||||
}.on('init'),
|
||||
|
||||
setupAssignmentWeightingScheme: function() {
|
||||
setupAssignmentWeightingScheme: function () {
|
||||
this.set('weightingScheme', ENV.GRADEBOOK_OPTIONS.group_weighting_scheme)
|
||||
}.on('init'),
|
||||
|
||||
renderGradebookMenu: function() {
|
||||
renderGradebookMenu: function () {
|
||||
const mountPoint = document.querySelector('[data-component="GradebookSelector"]')
|
||||
if (!mountPoint) {
|
||||
return
|
||||
|
@ -627,7 +626,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
},
|
||||
|
||||
calculateAllGrades: function() {
|
||||
calculateAllGrades: function () {
|
||||
return this.get('students').forEach(student => this.calculateStudentGrade(student))
|
||||
}.observes(
|
||||
'includeUngradedAssignments',
|
||||
|
@ -648,7 +647,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
assignment_subtotals: [],
|
||||
subtotal_by_period: false,
|
||||
|
||||
fetchAssignmentGroups: function() {
|
||||
fetchAssignmentGroups: function () {
|
||||
const params = {exclude_response_fields: ['in_closed_grading_period', 'rubric']}
|
||||
const gpId = this.get('selectedGradingPeriod.id')
|
||||
if (this.get('has_grading_periods') && gpId !== '0') {
|
||||
|
@ -704,7 +703,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
})()
|
||||
},
|
||||
|
||||
assignmentSubtotals: function() {
|
||||
assignmentSubtotals: function () {
|
||||
const subtotals = []
|
||||
if (this.subtotalByGradingPeriod()) {
|
||||
this.pushGradingPeriods(subtotals)
|
||||
|
@ -754,7 +753,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return submissionGroups
|
||||
},
|
||||
|
||||
fetchStudentSubmissions: function() {
|
||||
fetchStudentSubmissions: function () {
|
||||
return Ember.run.once(() => {
|
||||
const notYetLoaded = this.get('students').filter(student => {
|
||||
if (get(student, 'isLoaded') || get(student, 'isLoading')) {
|
||||
|
@ -782,7 +781,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.observes('students.@each', 'selectedGradingPeriod')
|
||||
.on('init'),
|
||||
|
||||
showNotesColumn: function() {
|
||||
showNotesColumn: function () {
|
||||
const notes = this.get('teacherNotes')
|
||||
if (notes) {
|
||||
return !notes.hidden
|
||||
|
@ -793,20 +792,20 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.property()
|
||||
.volatile(),
|
||||
|
||||
shouldCreateNotes: function() {
|
||||
shouldCreateNotes: function () {
|
||||
return !this.get('teacherNotes') && this.get('showNotesColumn')
|
||||
}.property('teacherNotes', 'showNotesColumn', 'custom_columns.@each'),
|
||||
|
||||
notesURL: function() {
|
||||
notesURL: function () {
|
||||
if (this.get('shouldCreateNotes')) {
|
||||
return window.ENV.GRADEBOOK_OPTIONS.custom_columns_url
|
||||
} else {
|
||||
const notesID = __guard__(this.get('teacherNotes'), x => x.id)
|
||||
return window.ENV.GRADEBOOK_OPTIONS.custom_column_url.replace(/:id/, notesID);
|
||||
return window.ENV.GRADEBOOK_OPTIONS.custom_column_url.replace(/:id/, notesID)
|
||||
}
|
||||
}.property('shouldCreateNotes', 'custom_columns.@each'),
|
||||
|
||||
notesParams: function() {
|
||||
notesParams: function () {
|
||||
if (this.get('shouldCreateNotes')) {
|
||||
return {
|
||||
'column[title]': I18n.t('notes', 'Notes'),
|
||||
|
@ -818,7 +817,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('shouldCreateNotes', 'showNotesColumn'),
|
||||
|
||||
notesVerb: function() {
|
||||
notesVerb: function () {
|
||||
if (this.get('shouldCreateNotes')) {
|
||||
return 'POST'
|
||||
} else {
|
||||
|
@ -826,7 +825,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('shouldCreateNotes'),
|
||||
|
||||
updateOrCreateNotesColumn: function() {
|
||||
updateOrCreateNotesColumn: function () {
|
||||
return ajax
|
||||
.request({
|
||||
dataType: 'json',
|
||||
|
@ -837,7 +836,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.then(this.boundNotesSuccess)
|
||||
}.observes('showNotesColumn'),
|
||||
|
||||
bindNotesSuccess: function() {
|
||||
bindNotesSuccess: function () {
|
||||
return (this.boundNotesSuccess = this.onNotesUpdateSuccess.bind(this))
|
||||
}.on('init'),
|
||||
|
||||
|
@ -862,7 +861,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
},
|
||||
|
||||
groupsAreWeighted: function() {
|
||||
groupsAreWeighted: function () {
|
||||
return this.get('weightingScheme') === 'percent'
|
||||
}.property('weightingScheme'),
|
||||
|
||||
|
@ -870,15 +869,15 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return !!__guard__(this.getGradingPeriodSet(), x => x.weighted)
|
||||
},
|
||||
|
||||
gradesAreWeighted: function() {
|
||||
gradesAreWeighted: function () {
|
||||
return this.get('groupsAreWeighted') || this.periodsAreWeighted()
|
||||
}.property('weightingScheme'),
|
||||
|
||||
hidePointsPossibleForFinalGrade: function() {
|
||||
hidePointsPossibleForFinalGrade: function () {
|
||||
return !!(this.get('groupsAreWeighted') || this.subtotalByGradingPeriod())
|
||||
}.property('weightingScheme', 'selectedGradingPeriod'),
|
||||
|
||||
updateShowTotalAs: function() {
|
||||
updateShowTotalAs: function () {
|
||||
this.set('showTotalAsPoints', this.get('showTotalAsPoints'))
|
||||
return ajax.request({
|
||||
dataType: 'json',
|
||||
|
@ -930,7 +929,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
})
|
||||
},
|
||||
|
||||
dataForStudent: function() {
|
||||
dataForStudent: function () {
|
||||
const selectedStudent = this.get('selectedStudent')
|
||||
if (selectedStudent == null) {
|
||||
return
|
||||
|
@ -938,7 +937,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return this.get('studentColumnData')[selectedStudent.id]
|
||||
}.property('selectedStudent', 'custom_columns.@each.isLoaded'),
|
||||
|
||||
loadCustomColumnData: function() {
|
||||
loadCustomColumnData: function () {
|
||||
if (!this.get('enrollments.isLoaded')) {
|
||||
return
|
||||
}
|
||||
|
@ -953,7 +952,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.forEach(col => this.fetchColumnData(col))
|
||||
}.observes('enrollments.isLoaded', 'custom_columns.@each'),
|
||||
|
||||
studentsInSelectedSection: function() {
|
||||
studentsInSelectedSection: function () {
|
||||
const students = this.get('students')
|
||||
const currentSection = this.get('selectedSection')
|
||||
|
||||
|
@ -970,21 +969,21 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}, {})
|
||||
},
|
||||
|
||||
submissionsLoaded: function() {
|
||||
submissionsLoaded: function () {
|
||||
const assignments = this.get('assignmentsFromGroups')
|
||||
const assignmentsByID = this.groupById(assignments)
|
||||
const studentsByID = this.groupById(this.get('students'))
|
||||
const submissions = this.get('submissions') || []
|
||||
submissions.forEach(function(submission) {
|
||||
submissions.forEach(function (submission) {
|
||||
const student = studentsByID[submission.user_id]
|
||||
if (student) {
|
||||
submission.submissions.forEach(function(s) {
|
||||
submission.submissions.forEach(function (s) {
|
||||
const assignment = assignmentsByID[s.assignment_id]
|
||||
set(s, 'hidden', !this.differentiatedAssignmentVisibleToStudent(assignment, s.user_id))
|
||||
return this.updateSubmission(s, student)
|
||||
}, this)
|
||||
// fill in hidden ones
|
||||
assignments.forEach(function(a) {
|
||||
assignments.forEach(function (a) {
|
||||
if (!this.differentiatedAssignmentVisibleToStudent(a, student.id)) {
|
||||
const sub = {
|
||||
user_id: student.id,
|
||||
|
@ -1071,34 +1070,33 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return pointsPossible === 0
|
||||
},
|
||||
|
||||
checkForInvalidGroups: function() {
|
||||
checkForInvalidGroups: function () {
|
||||
return this.get('assignment_groups').forEach(ag =>
|
||||
set(ag, 'invalid', this.checkForNoPointsWarning(ag))
|
||||
)
|
||||
}.observes('assignment_groups.@each'),
|
||||
|
||||
invalidAssignmentGroups: function() {
|
||||
invalidAssignmentGroups: function () {
|
||||
return this.get('assignment_groups').filterProperty('invalid', true)
|
||||
}.property('assignment_groups.@each.invalid'),
|
||||
|
||||
showInvalidGroupWarning: function() {
|
||||
showInvalidGroupWarning: function () {
|
||||
return (
|
||||
this.get('invalidAssignmentGroups').length > 0 && this.get('weightingScheme') === 'percent'
|
||||
)
|
||||
}.property('invalidAssignmentGroups', 'weightingScheme'),
|
||||
|
||||
invalidGroupNames: function() {
|
||||
invalidGroupNames: function () {
|
||||
return this.get('invalidAssignmentGroups').map(group => group.name)
|
||||
}
|
||||
.property('invalidAssignmentGroups')
|
||||
.readOnly(),
|
||||
|
||||
invalidGroupsWarningPhrases: function() {
|
||||
invalidGroupsWarningPhrases: function () {
|
||||
return I18n.t(
|
||||
'invalid_group_warning',
|
||||
{
|
||||
one:
|
||||
'Note: Score does not include assignments from the group %{list_of_group_names} because it has no points possible.',
|
||||
one: 'Note: Score does not include assignments from the group %{list_of_group_names} because it has no points possible.',
|
||||
other:
|
||||
'Note: Score does not include assignments from the groups %{list_of_group_names} because they have no points possible.'
|
||||
},
|
||||
|
@ -1109,7 +1107,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
)
|
||||
}.property('invalidGroupNames'),
|
||||
|
||||
populateAssignmentsFromGroups: function() {
|
||||
populateAssignmentsFromGroups: function () {
|
||||
if (!this.get('assignment_groups.isLoaded') || !!this.get('assignment_groups.isLoading')) {
|
||||
return
|
||||
}
|
||||
|
@ -1135,7 +1133,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
})
|
||||
}.observes('assignment_groups.isLoaded', 'assignment_groups.isLoading'),
|
||||
|
||||
populateAssignments: function() {
|
||||
populateAssignments: function () {
|
||||
const assignmentsFromGroups = this.get('assignmentsFromGroups.content')
|
||||
const selectedStudent = this.get('selectedStudent')
|
||||
const submissionStateMap = this.get('submissionStateMap')
|
||||
|
@ -1164,7 +1162,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
this.set('assignments', proxy)
|
||||
}.observes('assignmentsFromGroups.isLoaded', 'selectedStudent'),
|
||||
|
||||
populateSubmissionStateMap: function() {
|
||||
populateSubmissionStateMap: function () {
|
||||
const map = new SubmissionStateMap({
|
||||
hasGradingPeriods: !!this.has_grading_periods,
|
||||
selectedGradingPeriodID: this.get('selectedGradingPeriod.id') || '0',
|
||||
|
@ -1178,7 +1176,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
'submissions.content.length'
|
||||
),
|
||||
|
||||
includeUngradedAssignments: function() {
|
||||
includeUngradedAssignments: function () {
|
||||
const localValue = userSettings.contextGet('include_ungraded_assignments') || false
|
||||
if (!this.saveViewUngradedAsZeroToServer()) {
|
||||
return localValue
|
||||
|
@ -1194,7 +1192,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
|
||||
showAttendance: (() => userSettings.contextGet('show_attendance')).property().volatile(),
|
||||
|
||||
updateIncludeUngradedAssignmentsSetting: function() {
|
||||
updateIncludeUngradedAssignmentsSetting: function () {
|
||||
if (this.saveViewUngradedAsZeroToServer()) {
|
||||
ajax.request({
|
||||
dataType: 'json',
|
||||
|
@ -1239,7 +1237,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
],
|
||||
|
||||
assignmentSort: function(key, value) {
|
||||
assignmentSort: function (key, value) {
|
||||
const savedSortType = userSettings.contextGet('sort_grade_columns_by')
|
||||
const savedSortOption = this.get('assignmentSortOptions').findBy(
|
||||
'value',
|
||||
|
@ -1256,7 +1254,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property(),
|
||||
|
||||
sortAssignments: function() {
|
||||
sortAssignments: function () {
|
||||
const sort = this.get('assignmentSort')
|
||||
if (!sort) {
|
||||
return
|
||||
|
@ -1279,7 +1277,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
.observes('assignmentSort')
|
||||
.on('init'),
|
||||
|
||||
updateAssignmentStatusInGradingPeriod: function() {
|
||||
updateAssignmentStatusInGradingPeriod: function () {
|
||||
const assignment = this.get('selectedAssignment')
|
||||
|
||||
if (!assignment) {
|
||||
|
@ -1299,7 +1297,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.observes('selectedAssignment'),
|
||||
|
||||
selectedSubmission: function(key, selectedSubmission) {
|
||||
selectedSubmission: function (key, selectedSubmission) {
|
||||
if (arguments.length > 1) {
|
||||
this.set('selectedStudent', this.get('students').findBy('id', selectedSubmission.user_id))
|
||||
this.set(
|
||||
|
@ -1325,22 +1323,23 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
? this.submissionStateMap.getSubmissionState(selectedSubmission)
|
||||
: undefined) || {}
|
||||
selectedSubmission.gradeLocked = submissionState.locked
|
||||
selectedSubmission[selectedSubmission.late_policy_status] = true
|
||||
return selectedSubmission
|
||||
}.property('selectedStudent', 'selectedAssignment'),
|
||||
|
||||
selectedSubmissionHidden: function() {
|
||||
selectedSubmissionHidden: function () {
|
||||
return this.get('selectedSubmission.hidden') || false
|
||||
}.property('selectedStudent', 'selectedAssignment'),
|
||||
|
||||
anonymizeStudents: function() {
|
||||
anonymizeStudents: function () {
|
||||
return this.get('selectedAssignment.anonymize_students')
|
||||
}.property('selectedAssignment'),
|
||||
|
||||
selectedSubmissionLate: function() {
|
||||
selectedSubmissionLate: function () {
|
||||
return (this.get('selectedSubmission.points_deducted') || 0) > 0
|
||||
}.property('selectedStudent', 'selectedAssignment'),
|
||||
|
||||
selectedOutcomeResult: function() {
|
||||
selectedOutcomeResult: function () {
|
||||
if (this.get('selectedStudent') == null || this.get('selectedOutcome') == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -1360,15 +1359,15 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
)
|
||||
}.property('selectedStudent', 'selectedOutcome'),
|
||||
|
||||
outcomeResultIsDefined: function() {
|
||||
outcomeResultIsDefined: function () {
|
||||
return __guard__(this.get('selectedOutcomeResult'), x => x.score) != null
|
||||
}.property('selectedOutcomeResult'),
|
||||
|
||||
showAssignmentPointsWarning: function() {
|
||||
showAssignmentPointsWarning: function () {
|
||||
return this.get('selectedAssignment.noPointsPossibleWarning') && this.get('groupsAreWeighted')
|
||||
}.property('selectedAssignment', 'groupsAreWeighted'),
|
||||
|
||||
selectedStudentSections: function() {
|
||||
selectedStudentSections: function () {
|
||||
const student = this.get('selectedStudent')
|
||||
const sections = this.get('sections')
|
||||
if (!sections.isLoaded || student == null) {
|
||||
|
@ -1378,7 +1377,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return sectionNames.join(', ')
|
||||
}.property('selectedStudent', 'sections.isLoaded'),
|
||||
|
||||
assignmentDetails: function() {
|
||||
assignmentDetails: function () {
|
||||
if (this.get('selectedAssignment') == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -1392,7 +1391,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return locals
|
||||
}.property('selectedAssignment', 'students.@each.total_grade'),
|
||||
|
||||
outcomeDetails: function() {
|
||||
outcomeDetails: function () {
|
||||
if (this.get('selectedOutcome') == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -1409,7 +1408,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('selectedOutcome', 'outcome_rollups'),
|
||||
|
||||
calculationDetails: function() {
|
||||
calculationDetails: function () {
|
||||
if (this.get('selectedOutcome') == null) {
|
||||
return null
|
||||
}
|
||||
|
@ -1423,7 +1422,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
)
|
||||
}.property('selectedOutcome'),
|
||||
|
||||
assignmentSubmissionTypes: function() {
|
||||
assignmentSubmissionTypes: function () {
|
||||
const types = this.get('selectedAssignment.submission_types')
|
||||
const submissionTypes = this.get('submissionTypes')
|
||||
if (types === undefined || types.length === 0) {
|
||||
|
@ -1449,7 +1448,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
media_recording: I18n.t('media_recordin', 'Media recording')
|
||||
},
|
||||
|
||||
assignmentIndex: function() {
|
||||
assignmentIndex: function () {
|
||||
const selected = this.get('selectedAssignment')
|
||||
if (selected) {
|
||||
return this.get('assignments').indexOf(selected)
|
||||
|
@ -1458,7 +1457,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('selectedAssignment', 'assignmentSort'),
|
||||
|
||||
studentIndex: function() {
|
||||
studentIndex: function () {
|
||||
const selected = this.get('selectedStudent')
|
||||
if (selected) {
|
||||
return this.get('studentsInSelectedSection').indexOf(selected)
|
||||
|
@ -1467,7 +1466,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('selectedStudent', 'selectedSection'),
|
||||
|
||||
outcomeIndex: function() {
|
||||
outcomeIndex: function () {
|
||||
const selected = this.get('selectedOutcome')
|
||||
if (selected) {
|
||||
return this.get('outcomes').indexOf(selected)
|
||||
|
@ -1476,7 +1475,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('selectedOutcome'),
|
||||
|
||||
displayName: function() {
|
||||
displayName: function () {
|
||||
if (this.get('hideStudentNames')) {
|
||||
return 'hiddenName'
|
||||
} else {
|
||||
|
@ -1484,7 +1483,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
}
|
||||
}.property('hideStudentNames'),
|
||||
|
||||
fetchCorrectEnrollments: function() {
|
||||
fetchCorrectEnrollments: function () {
|
||||
let url
|
||||
if (this.get('enrollments.isLoading')) {
|
||||
return
|
||||
|
@ -1500,7 +1499,7 @@ const ScreenreaderGradebookController = Ember.ObjectController.extend({
|
|||
return fetchAllPages(url, {records: enrollments})
|
||||
}.observes('showConcludedEnrollments'),
|
||||
|
||||
omitFromFinalGrade: function() {
|
||||
omitFromFinalGrade: function () {
|
||||
return this.get('selectedAssignment.omit_from_final_grade')
|
||||
}.property('selectedAssignment')
|
||||
})
|
||||
|
|
|
@ -27,6 +27,13 @@
|
|||
</ul>
|
||||
</span>
|
||||
{{/if}}
|
||||
{{#if selectedSubmission.extended}}
|
||||
<span class="extended-pill">
|
||||
<ul class="pill pill-align error" />
|
||||
<li class="error"><strong>{{#t}}extended{{/t}}</strong></li>
|
||||
</ul>
|
||||
</span>
|
||||
{{/if}}
|
||||
</label>
|
||||
</div>
|
||||
{{
|
||||
|
|
|
@ -35,6 +35,8 @@ export function determineSubmissionSelection(submission) {
|
|||
return 'missing'
|
||||
} else if (submission.late) {
|
||||
return 'late'
|
||||
} else if (submission.late_policy_status === 'extended') {
|
||||
return 'extended'
|
||||
} else {
|
||||
return 'none'
|
||||
}
|
||||
|
|
|
@ -331,27 +331,27 @@ function sectionSelectionOptions(
|
|||
}
|
||||
|
||||
function mergeStudentsAndSubmission() {
|
||||
window.jsonData.studentsWithSubmissions = window.jsonData.context.students
|
||||
window.jsonData.studentMap = {}
|
||||
window.jsonData.studentEnrollmentMap = {}
|
||||
window.jsonData.studentSectionIdsMap = {}
|
||||
window.jsonData.submissionsMap = {}
|
||||
const jsonData = window.jsonData
|
||||
|
||||
window.jsonData.context.enrollments.forEach(enrollment => {
|
||||
jsonData.studentsWithSubmissions = jsonData.context.students
|
||||
jsonData.studentMap = {}
|
||||
jsonData.studentEnrollmentMap = {}
|
||||
jsonData.studentSectionIdsMap = {}
|
||||
jsonData.submissionsMap = {}
|
||||
|
||||
jsonData.context.enrollments.forEach(enrollment => {
|
||||
const enrollmentAnonymizableUserId = enrollment[anonymizableUserId]
|
||||
window.jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId] =
|
||||
window.jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId] || []
|
||||
window.jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId] =
|
||||
window.jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId] || {}
|
||||
jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId] =
|
||||
jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId] || []
|
||||
jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId] =
|
||||
jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId] || {}
|
||||
|
||||
window.jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId].push(enrollment)
|
||||
window.jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId][
|
||||
enrollment.course_section_id
|
||||
] = true
|
||||
jsonData.studentEnrollmentMap[enrollmentAnonymizableUserId].push(enrollment)
|
||||
jsonData.studentSectionIdsMap[enrollmentAnonymizableUserId][enrollment.course_section_id] = true
|
||||
})
|
||||
|
||||
window.jsonData.submissions.forEach(submission => {
|
||||
window.jsonData.submissionsMap[submission[anonymizableUserId]] = submission
|
||||
jsonData.submissions.forEach(submission => {
|
||||
jsonData.submissionsMap[submission[anonymizableUserId]] = submission
|
||||
})
|
||||
|
||||
window.jsonData.studentsWithSubmissions = window.jsonData.studentsWithSubmissions.reduce(
|
||||
|
@ -379,12 +379,12 @@ function mergeStudentsAndSubmission() {
|
|||
|
||||
// need to presort by anonymous_id for anonymous assignments so that the index property can be consistent
|
||||
if (isAnonymous)
|
||||
window.jsonData.studentsWithSubmissions.sort((a, b) =>
|
||||
jsonData.studentsWithSubmissions.sort((a, b) =>
|
||||
a.anonymous_name_position > b.anonymous_name_position ? 1 : -1
|
||||
)
|
||||
|
||||
// handle showing students only in a certain section.
|
||||
if (!window.jsonData.GROUP_GRADING_MODE) {
|
||||
if (!jsonData.GROUP_GRADING_MODE) {
|
||||
sectionToShow = ENV.selected_section_id
|
||||
}
|
||||
|
||||
|
@ -394,12 +394,12 @@ function mergeStudentsAndSubmission() {
|
|||
if (sectionToShow) {
|
||||
sectionToShow = sectionToShow.toString()
|
||||
|
||||
const studentsInSection = window.jsonData.studentsWithSubmissions.filter(student =>
|
||||
const studentsInSection = jsonData.studentsWithSubmissions.filter(student =>
|
||||
student.section_ids.includes(sectionToShow)
|
||||
)
|
||||
|
||||
if (studentsInSection.length > 0) {
|
||||
window.jsonData.studentsWithSubmissions = studentsInSection
|
||||
jsonData.studentsWithSubmissions = studentsInSection
|
||||
} else {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(
|
||||
|
@ -412,11 +412,11 @@ function mergeStudentsAndSubmission() {
|
|||
}
|
||||
}
|
||||
|
||||
window.jsonData.studentMap = _.keyBy(window.jsonData.studentsWithSubmissions, anonymizableId)
|
||||
jsonData.studentMap = _.keyBy(jsonData.studentsWithSubmissions, anonymizableId)
|
||||
|
||||
switch (userSettings.get('eg_sort_by')) {
|
||||
case 'submitted_at': {
|
||||
window.jsonData.studentsWithSubmissions.sort(
|
||||
jsonData.studentsWithSubmissions.sort(
|
||||
EG.compareStudentsBy(student => {
|
||||
const submittedAt = student && student.submission && student.submission.submitted_at
|
||||
if (submittedAt) {
|
||||
|
@ -438,7 +438,7 @@ function mergeStudentsAndSubmission() {
|
|||
graded: 4,
|
||||
not_gradeable: 5
|
||||
}
|
||||
window.jsonData.studentsWithSubmissions.sort(
|
||||
jsonData.studentsWithSubmissions.sort(
|
||||
EG.compareStudentsBy(
|
||||
student =>
|
||||
student && states[SpeedgraderHelpers.submissionState(student, ENV.grading_role)]
|
||||
|
@ -1201,7 +1201,7 @@ function statusMenuComponent(submission) {
|
|||
|
||||
function getLateMissingAndExcusedPills() {
|
||||
return document.querySelectorAll(
|
||||
'.submission-missing-pill, .submission-late-pill, .submission-excused-pill'
|
||||
'.submission-missing-pill, .submission-late-pill, .submission-excused-pill, .submission-extended-pill'
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -2499,11 +2499,15 @@ EG = {
|
|||
grade = GradeFormatHelper.formatGrade(s.grade)
|
||||
}
|
||||
|
||||
const late_policy_status =
|
||||
(s.late && 'late') || (s.missing && 'missing') || s.late_policy_status
|
||||
|
||||
return {
|
||||
value: i,
|
||||
late: s.late,
|
||||
missing: s.missing,
|
||||
excused: s.excused,
|
||||
late_policy_status,
|
||||
selected: selectedIndex === i,
|
||||
submittedAt: $.datetimeString(s.submitted_at) || noSubmittedAt,
|
||||
grade
|
||||
|
|
|
@ -3,16 +3,11 @@
|
|||
{{#if ../showSubmissionStatus}}
|
||||
{{#t "submitted"}}<label>Submitted:</label> {{submittedAt}}{{/t}}
|
||||
{{/if}}
|
||||
{{#if late}}
|
||||
<span class="submission-late-pill"></span>
|
||||
{{else}}
|
||||
{{#if missing}}
|
||||
<span class="submission-missing-pill"></span>
|
||||
{{else}}
|
||||
{{#if excused}}
|
||||
<span class="submission-excused-pill"></span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if late_policy_status}}
|
||||
<span class="submission-status-pill submission-{{late_policy_status}}-pill"></span>
|
||||
{{/if}}
|
||||
{{#if excused}}
|
||||
<span class="submission-status-pill submission-excused-pill"></span>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{else}}
|
||||
|
|
|
@ -29,6 +29,7 @@ import TimeLateInput from '@canvas/grading/TimeLateInput'
|
|||
const I18n = useI18nScope('speed_grader')
|
||||
|
||||
const statusesMap = {
|
||||
extended: I18n.t('Extended'),
|
||||
excused: I18n.t('Excused'),
|
||||
late: I18n.t('Late'),
|
||||
missing: I18n.t('Missing'),
|
||||
|
@ -49,7 +50,10 @@ export default function SpeedGraderStatusMenu(props) {
|
|||
props.updateSubmission(data)
|
||||
}
|
||||
|
||||
const menuOptions = ['late', 'missing', 'excused', 'none'].map(status => (
|
||||
const optionValues = ['late', 'missing', 'excused', 'none']
|
||||
if (ENV.FEATURES && ENV.FEATURES.extended_submission_state) optionValues.splice(3, 0, 'extended')
|
||||
|
||||
const menuOptions = optionValues.map(status => (
|
||||
<Menu.Item
|
||||
key={status}
|
||||
value={status}
|
||||
|
|
|
@ -26,6 +26,8 @@ describe('SpeedGraderStatusMenu', () => {
|
|||
const renderComponent = () => render(<SpeedGraderStatusMenu {...props} />)
|
||||
|
||||
beforeEach(() => {
|
||||
window.ENV = {FEATURES: {}}
|
||||
|
||||
props = {
|
||||
lateSubmissionInterval: 'day',
|
||||
locale: 'en',
|
||||
|
|
|
@ -38,6 +38,12 @@ export default function SubmissionStatusPill(props) {
|
|||
{I18n.t('Late')}
|
||||
</Pill>
|
||||
)
|
||||
} else if (props.submissionStatus === 'extended') {
|
||||
return (
|
||||
<Pill data-test-id="extended-pill" color="info">
|
||||
{I18n.t('Extended')}
|
||||
</Pill>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ export default {
|
|||
const missMountPoints = document.querySelectorAll('.submission-missing-pill')
|
||||
const lateMountPoints = document.querySelectorAll('.submission-late-pill')
|
||||
const excusedMountPoints = document.querySelectorAll('.submission-excused-pill')
|
||||
const extendedMountPoints = document.querySelectorAll('.submission-extended-pill')
|
||||
|
||||
forEachNode(missMountPoints, mountPoint => {
|
||||
ReactDOM.render(<Pill color="danger">{I18n.t('missing')}</Pill>, mountPoint)
|
||||
|
@ -46,5 +47,9 @@ export default {
|
|||
forEachNode(excusedMountPoints, mountPoint => {
|
||||
ReactDOM.render(<Pill color="danger">{I18n.t('excused')}</Pill>, mountPoint)
|
||||
})
|
||||
|
||||
forEachNode(extendedMountPoints, mountPoint => {
|
||||
ReactDOM.render(<Pill color="alert">{I18n.t('extended')}</Pill>, mountPoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue