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:
Ethan Knapp 2022-04-29 14:09:22 -06:00
parent 703b5d88a6
commit 947fc1b84a
36 changed files with 314 additions and 149 deletions

View File

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

View File

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

View File

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

View File

@ -91,6 +91,7 @@ module Interfaces::SubmissionInterface
graphql_name "LatePolicyStatusType"
value "late"
value "missing"
value "extended"
value "none"
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -45,7 +45,8 @@ GridColor.propTypes = {
missing: string,
resubmitted: string,
dropped: string,
excused: string
excused: string,
extended: string
}).isRequired,
statuses: arrayOf(string)
}

View File

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

View File

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

View File

@ -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', () => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,8 @@ describe('SpeedGraderStatusMenu', () => {
const renderComponent = () => render(<SpeedGraderStatusMenu {...props} />)
beforeEach(() => {
window.ENV = {FEATURES: {}}
props = {
lateSubmissionInterval: 'day',
locale: 'en',

View File

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

View File

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