From 2bace944b80c5e594966eed44f05278ad50142c0 Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Tue, 16 Aug 2016 17:13:00 -0500 Subject: [PATCH 01/10] Revert "fix grading period close date to match end date" This reverts commit 487caf8da4dba55c2ba464915e3b948a2a84482b. Change-Id: I7ee44dfa0ab6109b15849b175e4996773f87587e Reviewed-on: https://gerrit.instructure.com/87981 Tested-by: Jenkins Reviewed-by: Keith T. Garner Reviewed-by: Spencer Olson Product-Review: Spencer Olson QA-Review: Spencer Olson --- app/models/grading_period.rb | 4 ++-- spec/models/grading_period_spec.rb | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 4b808d6dff2..533db78b6de 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -29,7 +29,7 @@ class GradingPeriod < ActiveRecord::Base validate :close_date_is_not_before_end_date validate :not_overlapping, unless: :skip_not_overlapping_validator? - before_validation :ensure_close_date + before_validation :ensure_close_date, on: :create scope :current, -> do where("start_date <= :now AND end_date >= :now", now: Time.zone.now) @@ -170,7 +170,7 @@ class GradingPeriod < ActiveRecord::Base end def ensure_close_date - self.close_date = self.end_date + self.close_date ||= self.end_date end def close_date_is_not_before_end_date diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index 7ba282c27df..bd31e9b614f 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -66,13 +66,11 @@ describe GradingPeriod do end it "allows setting a close_date that is different from the end_date" do - skip grading_period = grading_period_group.grading_periods.create!(params) expect(grading_period.close_date).not_to eq(grading_period.end_date) end it "considers the grading period invalid if the close date is before the end date" do - skip period_params = params.merge(close_date: 1.day.ago(params[:end_date])) grading_period = grading_period_group.grading_periods.build(period_params) expect(grading_period).to be_invalid From a96f8e42413b2ffb4470193256c92f4b3257ba8c Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Tue, 16 Aug 2016 17:16:01 -0500 Subject: [PATCH 02/10] Revert "Revert close date commits" This reverts commit d3238e02517c3871ac265b82b40e2b662031b93d. Change-Id: If50d495b208817362792d3a93f71c49724ce586b Reviewed-on: https://gerrit.instructure.com/87982 Tested-by: Jenkins Reviewed-by: Spencer Olson Product-Review: Spencer Olson QA-Review: Spencer Olson Reviewed-by: Jeremy Neander --- .../api/gradingPeriodSetsApi.coffee | 3 + .../api/gradingPeriodsApi.coffee | 4 + app/jsx/grading/AccountGradingPeriod.jsx | 42 ++-- app/jsx/grading/GradingPeriodForm.jsx | 97 +++++--- app/jsx/grading/GradingPeriodSet.jsx | 16 +- app/jsx/grading/gradingPeriod.jsx | 26 ++- app/jsx/grading/gradingPeriodCollection.jsx | 55 +---- app/jsx/grading/gradingPeriodTemplate.jsx | 154 ++++++++----- app/models/grading_period.rb | 20 +- app/stylesheets/bundles/grading_periods.scss | 40 ++-- ...448_populate_grading_period_close_dates.rb | 8 + .../populate_grading_period_close_dates.rb | 8 + .../api/gradingPeriodSetsApiSpec.coffee | 61 +++-- .../api/gradingPeriodsApiSpec.coffee | 31 ++- .../GradingPeriodCollectionSpec.coffee | 80 ++++--- .../jsx/gradebook/GradingPeriodSpec.coffee | 88 ++++---- .../GradingPeriodTemplateSpec.coffee | 212 ++++++++---------- .../jsx/grading/AccountGradingPeriodSpec.jsx | 9 +- .../jsx/grading/GradingPeriodFormSpec.jsx | 94 +++++++- .../jsx/grading/GradingPeriodSetSpec.jsx | 85 +++++-- ...opulate_grading_period_close_dates_spec.rb | 29 +++ spec/models/grading_period_spec.rb | 56 +++-- spec/selenium/grading_periods_course_spec.rb | 5 - .../selenium/multiple_grading_periods_spec.rb | 34 --- 24 files changed, 740 insertions(+), 517 deletions(-) create mode 100644 db/migrate/20160707203448_populate_grading_period_close_dates.rb create mode 100644 lib/data_fixup/populate_grading_period_close_dates.rb create mode 100644 spec/lib/data_fixup/populate_grading_period_close_dates_spec.rb diff --git a/app/coffeescripts/api/gradingPeriodSetsApi.coffee b/app/coffeescripts/api/gradingPeriodSetsApi.coffee index abf615f5d06..7b79eaa7a31 100644 --- a/app/coffeescripts/api/gradingPeriodSetsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodSetsApi.coffee @@ -27,6 +27,9 @@ define [ title: period.title startDate: new Date(period.start_date) endDate: new Date(period.end_date) + # TODO: After the close_date data fixup has run, this can become: + # `closeDate: new Date(period.close_date)` + closeDate: new Date(period.close_date || period.end_date) } baseDeserializeSet = (set) -> diff --git a/app/coffeescripts/api/gradingPeriodsApi.coffee b/app/coffeescripts/api/gradingPeriodsApi.coffee index 4c460382b73..bf0e91155e0 100644 --- a/app/coffeescripts/api/gradingPeriodsApi.coffee +++ b/app/coffeescripts/api/gradingPeriodsApi.coffee @@ -14,6 +14,7 @@ define [ title: period.title start_date: period.startDate end_date: period.endDate + close_date: period.closeDate } grading_periods: serialized @@ -24,6 +25,9 @@ define [ title: period.title startDate: new Date(period.start_date) endDate: new Date(period.end_date) + # TODO: After the close_date data fixup has run, this can become: + # `closeDate: new Date(period.close_date)` + closeDate: new Date(period.close_date || period.end_date) } batchUpdate: (setId, periods) -> diff --git a/app/jsx/grading/AccountGradingPeriod.jsx b/app/jsx/grading/AccountGradingPeriod.jsx index 2cb4cb1e0c8..6a26c200445 100644 --- a/app/jsx/grading/AccountGradingPeriod.jsx +++ b/app/jsx/grading/AccountGradingPeriod.jsx @@ -6,27 +6,28 @@ define([ 'jsx/shared/helpers/dateHelper', 'jquery.instructure_misc_helpers' ], function(React, $, axios, I18n, DateHelper) { - const types = React.PropTypes; + const Types = React.PropTypes; let AccountGradingPeriod = React.createClass({ propTypes: { - period: types.shape({ - id: types.string.isRequired, - title: types.string.isRequired, - startDate: types.instanceOf(Date).isRequired, - endDate: types.instanceOf(Date).isRequired + period: Types.shape({ + id: Types.string.isRequired, + title: Types.string.isRequired, + startDate: Types.instanceOf(Date).isRequired, + endDate: Types.instanceOf(Date).isRequired, + closeDate: Types.instanceOf(Date).isRequired }).isRequired, - onEdit: types.func.isRequired, - actionsDisabled: types.bool, - readOnly: types.bool.isRequired, - permissions: types.shape({ - read: types.bool.isRequired, - create: types.bool.isRequired, - update: types.bool.isRequired, - delete: types.bool.isRequired + onEdit: Types.func.isRequired, + actionsDisabled: Types.bool, + readOnly: Types.bool.isRequired, + permissions: Types.shape({ + read: Types.bool.isRequired, + create: Types.bool.isRequired, + update: Types.bool.isRequired, + delete: Types.bool.isRequired }).isRequired, - onDelete: types.func.isRequired, - deleteGradingPeriodURL: types.string.isRequired + onDelete: Types.func.isRequired, + deleteGradingPeriodURL: Types.string.isRequired }, promptDeleteGradingPeriod(event) { @@ -85,15 +86,18 @@ define([ return (
-
+
{this.props.period.title}
-
+
{I18n.t("Start Date:")} {DateHelper.formatDatetimeForDisplay(this.props.period.startDate)}
-
+
{I18n.t("End Date:")} {DateHelper.formatDatetimeForDisplay(this.props.period.endDate)}
+
+ {I18n.t("Close Date:")} {DateHelper.formatDatetimeForDisplay(this.props.period.closeDate)} +
{this.renderEditButton()} diff --git a/app/jsx/grading/GradingPeriodForm.jsx b/app/jsx/grading/GradingPeriodForm.jsx index e3337730ed1..b0efe234fa6 100644 --- a/app/jsx/grading/GradingPeriodForm.jsx +++ b/app/jsx/grading/GradingPeriodForm.jsx @@ -6,33 +6,70 @@ define([ 'jsx/due_dates/DueDateCalendarPicker', 'jsx/shared/helpers/accessibleDateFormat' ], function(React, ReactDOM, _, I18n, DueDateCalendarPicker, accessibleDateFormat) { - const types = React.PropTypes; + const update = React.addons.update; + const Types = React.PropTypes; const buildPeriod = function(attr) { return { id: attr.id, title: attr.title, - startDate: attr.startDate || new Date(''), - endDate: attr.endDate || new Date('') + startDate: attr.startDate, + endDate: attr.endDate, + closeDate: attr.closeDate }; }; + const hasDistinctCloseDate = ({ endDate, closeDate }) => { + return closeDate && !_.isEqual(endDate, closeDate); + }; + + const mergePeriod = (form, attr) => { + return update(form.state.period, {$merge: attr}); + } + + const changeTitle = function(e) { + let period = mergePeriod(this, {title: e.target.value}); + this.setState({period: period}); + }; + + const changeStartDate = function(date) { + let period = mergePeriod(this, {startDate: date}); + this.setState({period: period}); + }; + + const changeEndDate = function(date) { + let attr = {endDate: date}; + if (!this.state.preserveCloseDate && !hasDistinctCloseDate(this.state.period)) { + attr.closeDate = date; + } + let period = mergePeriod(this, attr); + this.setState({period: period}); + }; + + const changeCloseDate = function(date) { + let period = mergePeriod(this, {closeDate: date}); + this.setState({period: period, preserveCloseDate: !!date}); + }; + let GradingPeriodForm = React.createClass({ propTypes: { - period: types.shape({ - id: types.string.isRequired, - title: types.string.isRequired, - startDate: types.instanceOf(Date).isRequired, - endDate: types.instanceOf(Date).isRequired + period: Types.shape({ + id: Types.string.isRequired, + title: Types.string.isRequired, + startDate: Types.instanceOf(Date).isRequired, + endDate: Types.instanceOf(Date).isRequired, + closeDate: Types.instanceOf(Date).isRequired }), - disabled: types.bool.isRequired, - onSave: types.func.isRequired, - onCancel: types.func.isRequired + disabled: Types.bool.isRequired, + onSave: Types.func.isRequired, + onCancel: Types.func.isRequired }, getInitialState: function() { + let period = buildPeriod(this.props.period || {}); return { - period: buildPeriod(this.props.period || {}) + period: period, + preserveCloseDate: hasDistinctCloseDate(period) }; }, @@ -56,7 +93,7 @@ define([ className='ic-Input' title={I18n.t('Grading Period Title')} defaultValue={this.state.period.title} - onChange={this.changeTitle} + onChange={changeTitle.bind(this)} type='text'/>
@@ -73,7 +110,7 @@ define([
@@ -87,12 +124,24 @@ define([
+ +
+ + +
@@ -124,24 +173,6 @@ define([ ); }, - changeTitle: function(e) { - let period = _.clone(this.state.period); - period.title = e.target.value; - this.setState({period: period}); - }, - - changeStartDate: function(date) { - let period = _.clone(this.state.period); - period.startDate = date; - this.setState({period: period}); - }, - - changeEndDate: function(date) { - let period = _.clone(this.state.period); - period.endDate = date; - this.setState({period: period}); - }, - triggerSave: function() { if (this.props.onSave) { this.props.onSave(this.state.period); diff --git a/app/jsx/grading/GradingPeriodSet.jsx b/app/jsx/grading/GradingPeriodSet.jsx index 897682cd536..a664d483846 100644 --- a/app/jsx/grading/GradingPeriodSet.jsx +++ b/app/jsx/grading/GradingPeriodSet.jsx @@ -38,21 +38,31 @@ define([ } let validDates = _.all(periods, (period) => { - return isValidDate(period.startDate) && isValidDate(period.endDate); + return isValidDate(period.startDate) && + isValidDate(period.endDate) && + isValidDate(period.closeDate); }); if (!validDates) { return [I18n.t('All dates fields must be present and formatted correctly')]; } - let orderedDates = _.all(periods, (period) => { + let orderedStartAndEndDates = _.all(periods, (period) => { return period.startDate < period.endDate; }); - if (!orderedDates) { + if (!orderedStartAndEndDates) { return [I18n.t('All start dates must be before the end date')]; } + let orderedEndAndCloseDates = _.all(periods, (period) => { + return period.endDate <= period.closeDate; + }); + + if (!orderedEndAndCloseDates) { + return [I18n.t('All close dates must be on or after the end date')]; + } + if (anyPeriodsOverlap(periods)) { return [I18n.t('Grading periods must not overlap')]; } diff --git a/app/jsx/grading/gradingPeriod.jsx b/app/jsx/grading/gradingPeriod.jsx index 13ef02598fc..830fdaf6c0a 100644 --- a/app/jsx/grading/gradingPeriod.jsx +++ b/app/jsx/grading/gradingPeriod.jsx @@ -7,21 +7,22 @@ define([ 'jsx/grading/gradingPeriodTemplate', 'jsx/shared/helpers/dateHelper' ], function(tz, React, $, I18n, _, GradingPeriodTemplate, DateHelper) { - var types = React.PropTypes; + var Types = React.PropTypes; var GradingPeriod = React.createClass({ propTypes: { - title: types.string.isRequired, - startDate: types.instanceOf(Date).isRequired, - endDate: types.instanceOf(Date).isRequired, - id: types.string.isRequired, - updateGradingPeriodCollection: types.func.isRequired, - onDeleteGradingPeriod: types.func.isRequired, - disabled: types.bool.isRequired, - readOnly: types.bool.isRequired, - permissions: types.shape({ - update: types.bool.isRequired, - delete: types.bool.isRequired, + title: Types.string.isRequired, + startDate: Types.instanceOf(Date).isRequired, + endDate: Types.instanceOf(Date).isRequired, + closeDate: Types.instanceOf(Date).isRequired, + id: Types.string.isRequired, + updateGradingPeriodCollection: Types.func.isRequired, + onDeleteGradingPeriod: Types.func.isRequired, + disabled: Types.bool.isRequired, + readOnly: Types.bool.isRequired, + permissions: Types.shape({ + update: Types.bool.isRequired, + delete: Types.bool.isRequired, }).isRequired }, @@ -82,6 +83,7 @@ define([ title={this.props.title} startDate={this.props.startDate} endDate={this.props.endDate} + closeDate={this.props.closeDate || this.props.endDate} permissions={this.props.permissions} disabled={this.props.disabled} readOnly={this.props.readOnly} diff --git a/app/jsx/grading/gradingPeriodCollection.jsx b/app/jsx/grading/gradingPeriodCollection.jsx index 359c7c0eb2f..2995c6046e2 100644 --- a/app/jsx/grading/gradingPeriodCollection.jsx +++ b/app/jsx/grading/gradingPeriodCollection.jsx @@ -14,10 +14,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { return state.periods !== null; }; - const canAddPeriods = (state) => { - return !state.readOnly && state.canAddNewPeriods; - }; - let GradingPeriodCollection = React.createClass({ propTypes: { @@ -30,7 +26,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { readOnly: false, disabled: false, saveDisabled: true, - canAddNewPeriods: false, canChangeGradingPeriodsSetting: false }; }, @@ -46,7 +41,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { self.setState({ periods: self.deserializePeriods(periods), readOnly: periods.grading_periods_read_only, - canAddNewPeriods: periods.can_create_grading_periods, canChangeGradingPeriodsSetting: periods.can_toggle_grading_periods, disabled: false, saveDisabled: _.isEmpty(periods.grading_periods) @@ -62,17 +56,11 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { let newPeriod = ConvertCase.camelize(period); newPeriod.startDate = new Date(period.start_date); newPeriod.endDate = new Date(period.end_date); + newPeriod.closeDate = new Date(period.close_date || period.end_date); return newPeriod; }); }, - componentDidUpdate: function(prevProps, prevState) { - if (prevState.periods) { - let removedAGradingPeriod = this.state.periods.length < prevState.periods.length; - if (removedAGradingPeriod && this.refs.addPeriodButton) this.refs.addPeriodButton.focus(); - } - }, - deleteGradingPeriod: function(id) { if (id.indexOf('new') > -1) { this.removeDeletedGradingPeriod(id); @@ -105,29 +93,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { this.setState({periods: newPeriods}); }, - getCreateGradingPeriodCSS: function() { - let cssClasses = 'center-md new-grading-period pad-box border border-round'; - if (!this.state.periods || this.state.periods.length === 0) { - cssClasses += ' no-active-grading-periods'; - } - - return cssClasses; - }, - - createNewGradingPeriod: function() { - if (!this.state.readOnly && this.state.canAddNewPeriods) { - let newPeriod = { - title: '', - startDate: new Date(''), - endDate: new Date(''), - id: _.uniqueId('new'), - permissions: { read: true, update: true, delete: true} - }; - let periods = update(this.state.periods, {$push: [newPeriod]}); - this.setState({periods: periods, saveDisabled: false}); - } - }, - getPeriodById: function(id) { return _.find(this.state.periods, period => period.id === id); }, @@ -246,7 +211,7 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { }, renderSaveButton: function() { - if (periodsAreLoaded(this.state) && !this.state.readOnly && _.all(this.state.periods, period => period.permissions.update || period.permissions.create)) { + if (periodsAreLoaded(this.state) && !this.state.readOnly && _.all(this.state.periods, period => period.permissions.update)) { let buttonText = this.state.disabled ? I18n.t('Updating') : I18n.t('Save'); return (
@@ -271,6 +236,7 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { title={period.title} startDate={period.startDate} endDate={period.endDate} + closeDate={period.closeDate} permissions={period.permissions} readOnly={this.state.readOnly} disabled={this.state.disabled} @@ -281,20 +247,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) { }); }, - renderAddPeriodButton: function() { - if (periodsAreLoaded(this.state) && canAddPeriods(this.state)) { - return ( -
- -
- ); - } - }, - render: function () { return (
@@ -304,7 +256,6 @@ function(React, GradingPeriod, $, I18n, _, ConvertCase) {
{this.renderGradingPeriods()}
- {this.renderAddPeriodButton()} {this.renderSaveButton()}
); diff --git a/app/jsx/grading/gradingPeriodTemplate.jsx b/app/jsx/grading/gradingPeriodTemplate.jsx index 574ca9ae310..d007c934b40 100644 --- a/app/jsx/grading/gradingPeriodTemplate.jsx +++ b/app/jsx/grading/gradingPeriodTemplate.jsx @@ -8,23 +8,52 @@ define([ 'jsx/shared/helpers/dateHelper', 'jquery.instructure_date_and_time' ], function(tz, React, ReactDOM, $, I18n, _, DateHelper) { - const types = React.PropTypes; + const Types = React.PropTypes; const postfixId = (text, { props }) => { return text + props.id; }; + const isEditable = ({ props }) => { + return props.permissions.update && !props.readOnly; + }; + + const tabbableDate = (ref, date) => { + let formattedDate = DateHelper.formatDatetimeForDisplay(date); + return { formattedDate }; + }; + + const renderActions = ({ props, onDeleteGradingPeriod }) => { + if (props.permissions.delete && !props.readOnly) { + let cssClasses = "Button Button--icon-action icon-delete-grading-period"; + if (props.disabled) cssClasses += " disabled"; + return ( +
+ +
+ ); + } + }; + let GradingPeriodTemplate = React.createClass({ propTypes: { - title: types.string.isRequired, - startDate: types.instanceOf(Date).isRequired, - endDate: types.instanceOf(Date).isRequired, - id: types.string.isRequired, - permissions: types.shape({ - update: types.bool.isRequired, - delete: types.bool.isRequired, + title: Types.string.isRequired, + startDate: Types.instanceOf(Date).isRequired, + endDate: Types.instanceOf(Date).isRequired, + closeDate: Types.instanceOf(Date).isRequired, + id: Types.string.isRequired, + permissions: Types.shape({ + update: Types.bool.isRequired, + delete: Types.bool.isRequired, }).isRequired, - readOnly: types.bool.isRequired, + readOnly: Types.bool.isRequired, requiredPropsIfEditable: function(props) { if (!props.permissions.update && !props.permissions.delete) return; @@ -68,34 +97,17 @@ define([ }, onDeleteGradingPeriod: function() { - this.props.onDeleteGradingPeriod(this.props.id); - }, - - renderDeleteButton: function() { - if (!this.props.permissions.delete || this.props.readOnly) return null; - let cssClasses = "Button Button--icon-action icon-delete-grading-period"; - if (this.props.disabled) cssClasses += " disabled"; - return ( -
-
-
-
- -
-
-
-
- ); + if (!this.props.disabled) { + this.props.onDeleteGradingPeriod(this.props.id); + } }, renderTitle: function() { - if (this.props.permissions.update && !this.props.readOnly) { + if (isEditable(this)) { return ( - {this.props.title} +
+ {I18n.t("Grading Period Name")} + {this.props.title}
); } }, renderStartDate: function() { - if (this.props.permissions.update && !this.props.readOnly) { + if (isEditable(this)) { return ( ); } else { return ( -
- {DateHelper.formatDatetimeForDisplay(this.props.startDate)} +
+ {I18n.t("Start Date")} + { tabbableDate("startDate", this.props.startDate) }
); } }, renderEndDate: function() { - if (this.props.permissions.update && !this.props.readOnly) { + if (isEditable(this)) { return ( - {DateHelper.formatDatetimeForDisplay(this.props.endDate)} +
+ {I18n.t("End Date")} + { tabbableDate("endDate", this.props.endDate) }
); } }, + renderCloseDate: function() { + let closeDate = isEditable(this) ? this.props.endDate : this.props.closeDate; + return ( +
+ {I18n.t("Close Date")} + { tabbableDate("closeDate", closeDate || this.props.endDate) } +
+ ); + }, + render: function () { return (
-
-
- - {this.renderTitle()} +
+
+
+ + {this.renderTitle()} +
+
+ + {this.renderStartDate()} +
+
+ + {this.renderEndDate()} +
+
+ + {this.renderCloseDate()} +
-
- - {this.renderStartDate()} -
-
- - {this.renderEndDate()} -
- {this.renderDeleteButton()}
+ + {renderActions(this)}
); } diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 533db78b6de..6bee5bbd387 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -24,12 +24,13 @@ class GradingPeriod < ActiveRecord::Base belongs_to :grading_period_group, inverse_of: :grading_periods has_many :grading_period_grades, dependent: :destroy - validates :title, :start_date, :end_date, :grading_period_group_id, presence: true + validates :title, :start_date, :end_date, :close_date, :grading_period_group_id, presence: true validate :start_date_is_before_end_date - validate :close_date_is_not_before_end_date + validate :close_date_is_on_or_after_end_date validate :not_overlapping, unless: :skip_not_overlapping_validator? - before_validation :ensure_close_date, on: :create + before_validation :adjust_close_date_for_course_period + before_validation :ensure_close_date scope :current, -> do where("start_date <= :now AND end_date >= :now", now: Time.zone.now) @@ -164,18 +165,21 @@ class GradingPeriod < ActiveRecord::Base def start_date_is_before_end_date if start_date && end_date && end_date < start_date - errors.add(:end_date, t('errors.invalid_grading_period_end_date', - 'Grading period end date precedes start date')) + errors.add(:end_date, t('must be after start date')) end end + def adjust_close_date_for_course_period + self.close_date = self.end_date if grading_period_group.present? && course_group? + end + def ensure_close_date self.close_date ||= self.end_date end - def close_date_is_not_before_end_date - if close_date && end_date && close_date < end_date - errors.add(:close_date, t('Grading period close date precedes end date')) + def close_date_is_on_or_after_end_date + if close_date.present? && end_date.present? && close_date < end_date + errors.add(:close_date, t('must be on or after end date')) end end end diff --git a/app/stylesheets/bundles/grading_periods.scss b/app/stylesheets/bundles/grading_periods.scss index bafb9c88e42..c1d4f53bc29 100644 --- a/app/stylesheets/bundles/grading_periods.scss +++ b/app/stylesheets/bundles/grading_periods.scss @@ -14,9 +14,7 @@ .grading-period:nth-child(n+2) { border-top: none; } - .input-grading-period-date { - width: 160px; - } + .icon-delete-grading-period::before { padding: 9px 0px; cursor: pointer; @@ -26,34 +24,30 @@ .icon-delete-grading-period { width: 20px; } - .buttons-grid-row { - width: 250px; - } label { font-weight: bold; } } -.new-grading-period { - border-right-style: dashed; - border-bottom-style: dashed; - border-left-style: dashed; - border-top-style: none; - border-width: 2px; - @include fontSize(16px); +.grading-period { + display: flex; +} - #add-period-button { - @include fontSize(16px); - &:hover { - text-decoration: none; - } - } - .grading-period-add-icon { - margin-right: 4px; +.GradingPeriod__Details { + flex: 1 1; +} + +.GradingPeriod__Detail { + width: 100%; + + &.ic-Input.hasDatepicker { + display: inline-block; + width: calc(100% - 46px); } } -.no-active-grading-periods { - border-top-style: dashed; +.GradingPeriod__Actions { + flex: 0 0; + margin-right: 12px; } diff --git a/db/migrate/20160707203448_populate_grading_period_close_dates.rb b/db/migrate/20160707203448_populate_grading_period_close_dates.rb new file mode 100644 index 00000000000..7a94b2b5452 --- /dev/null +++ b/db/migrate/20160707203448_populate_grading_period_close_dates.rb @@ -0,0 +1,8 @@ +class PopulateGradingPeriodCloseDates < ActiveRecord::Migration + tag :postdeploy + + def up + DataFixup::PopulateGradingPeriodCloseDates.run + end +end + diff --git a/lib/data_fixup/populate_grading_period_close_dates.rb b/lib/data_fixup/populate_grading_period_close_dates.rb new file mode 100644 index 00000000000..dee37cb9b3e --- /dev/null +++ b/lib/data_fixup/populate_grading_period_close_dates.rb @@ -0,0 +1,8 @@ +module DataFixup::PopulateGradingPeriodCloseDates + def self.run + GradingPeriod. + where(close_date: nil). + where.not(end_date: nil). + update_all("close_date=end_date") + end +end diff --git a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee index 24a40a49198..63373d8a626 100644 --- a/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodSetsApiSpec.coffee @@ -12,12 +12,14 @@ define [ id: "1", title: "Q1", startDate: new Date("2015-09-01T12:00:00Z"), - endDate: new Date("2015-10-31T12:00:00Z") + endDate: new Date("2015-10-31T12:00:00Z"), + closeDate: new Date("2015-11-07T12:00:00Z") },{ id: "2", title: "Q2", startDate: new Date("2015-11-01T12:00:00Z"), - endDate: new Date("2015-12-31T12:00:00Z") + endDate: new Date("2015-12-31T12:00:00Z"), + closeDate: new Date("2016-01-07T12:00:00Z") } ], permissions: { read: true, create: true, update: true, delete: true }, @@ -41,12 +43,14 @@ define [ id: "1", title: "Q1", start_date: new Date("2015-09-01T12:00:00Z"), - end_date: new Date("2015-10-31T12:00:00Z") + end_date: new Date("2015-10-31T12:00:00Z"), + close_date: new Date("2015-11-07T12:00:00Z") },{ id: "2", title: "Q2", start_date: new Date("2015-11-01T12:00:00Z"), - end_date: new Date("2015-12-31T12:00:00Z") + end_date: new Date("2015-12-31T12:00:00Z"), + close_date: new Date("2016-01-07T12:00:00Z") } ], permissions: { read: true, create: true, update: true, delete: true }, @@ -86,7 +90,7 @@ define [ start() @server.respond() - asyncTest "uses the creation date as the title if the grading period set does not have a title", -> + asyncTest "creates a title from the creation date when the set has no title", -> untitledSets = grading_period_sets: [ id: "1" @@ -95,21 +99,44 @@ define [ permissions: { read: true, create: true, update: true, delete: true } created_at: "2015-11-29T12:00:00Z" ] - - @server.respondWith "GET", /grading_period_sets/, [200, { "Content-Type":"application/json", "Link": @fakeHeaders }, JSON.stringify untitledSets] + jsonString = JSON.stringify(untitledSets) + @server.respondWith( + "GET", + /grading_period_sets/, + [200, { "Content-Type":"application/json", "Link": @fakeHeaders }, jsonString] + ) api.list() .then (sets) => equal sets[0].title, "Set created Nov 29, 2015" start() @server.respond() - # no fail for CheatDepaginator - # asyncTest "SKIPPED: rejects the promise upon errors", -> - # @server.respondWith "GET", /grading_period_sets/, [500, {"Content-Type":"application/json"}, "FAIL"] - # api.list().catch (error) => - # equal error, "FAIL" - # start() - # @server.respond() + asyncTest "uses the endDate as the closeDate when a period has no closeDate", -> + setsWithoutPeriodCloseDate = + grading_period_sets: [ + id: "1" + title: "Fall 2015" + grading_periods: [{ + id: "1", + title: "Q1", + start_date: new Date("2015-09-01T12:00:00Z"), + end_date: new Date("2015-10-31T12:00:00Z"), + close_date: null + }] + permissions: { read: true, create: true, update: true, delete: true } + created_at: "2015-11-29T12:00:00Z" + ] + jsonString = JSON.stringify(setsWithoutPeriodCloseDate) + @server.respondWith( + "GET", + /grading_period_sets/, + [200, { "Content-Type":"application/json", "Link": @fakeHeaders }, jsonString] + ) + api.list() + .then (sets) => + deepEqual sets[0].gradingPeriods[0].closeDate, new Date("2015-10-31T12:00:00Z") + start() + @server.respond() deserializedSetCreating = { title: "Fall 2015", @@ -190,12 +217,14 @@ define [ id: "1", title: "Q1", start_date: new Date("2015-09-01T12:00:00Z"), - end_date: new Date("2015-10-31T12:00:00Z") + end_date: new Date("2015-10-31T12:00:00Z"), + close_date: new Date("2015-11-07T12:00:00Z") },{ id: "2", title: "Q2", start_date: new Date("2015-11-01T12:00:00Z"), - end_date: new Date("2015-12-31T12:00:00Z") + end_date: new Date("2015-12-31T12:00:00Z"), + close_date: null } ], permissions: { read: true, create: true, update: true, delete: true } diff --git a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee index e785cdf0754..fddd0defaff 100644 --- a/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee +++ b/spec/coffeescripts/api/gradingPeriodsApiSpec.coffee @@ -9,12 +9,14 @@ define [ id: "1", title: "Q1", startDate: new Date("2015-09-01T12:00:00Z"), - endDate: new Date("2015-10-31T12:00:00Z") + endDate: new Date("2015-10-31T12:00:00Z"), + closeDate: new Date("2015-11-07T12:00:00Z") },{ id: "2", title: "Q2", startDate: new Date("2015-11-01T12:00:00Z"), - endDate: new Date("2015-12-31T12:00:00Z") + endDate: new Date("2015-12-31T12:00:00Z"), + closeDate: new Date("2016-01-07T12:00:00Z") } ] @@ -24,12 +26,14 @@ define [ id: "1", title: "Q1", start_date: new Date("2015-09-01T12:00:00Z"), - end_date: new Date("2015-10-31T12:00:00Z") + end_date: new Date("2015-10-31T12:00:00Z"), + close_date: new Date("2015-11-07T12:00:00Z") },{ id: "2", title: "Q2", start_date: new Date("2015-11-01T12:00:00Z"), - end_date: new Date("2015-12-31T12:00:00Z") + end_date: new Date("2015-12-31T12:00:00Z"), + close_date: new Date("2016-01-07T12:00:00Z") } ] } @@ -54,6 +58,25 @@ define [ deepEqual periods, deserializedPeriods start() + asyncTest "uses the endDate as the closeDate when a period has no closeDate", -> + periodsWithoutCloseDate = { + grading_periods: [ + { + id: "1", + title: "Q1", + start_date: new Date("2015-09-01T12:00:00Z"), + end_date: new Date("2015-10-31T12:00:00Z"), + close_date: null + } + ] + } + successPromise = new Promise (resolve) => resolve({ data: periodsWithoutCloseDate }) + @stub(axios, "patch").returns(successPromise) + api.batchUpdate(123, deserializedPeriods) + .then (periods) => + deepEqual periods[0].closeDate, new Date("2015-10-31T12:00:00Z") + start() + asyncTest "rejects the promise upon errors", -> failurePromise = new Promise (_, reject) => reject("FAIL") @stub(axios, "patch").returns(failurePromise) diff --git a/spec/coffeescripts/jsx/gradebook/GradingPeriodCollectionSpec.coffee b/spec/coffeescripts/jsx/gradebook/GradingPeriodCollectionSpec.coffee index c6600c1e2aa..5bf71c0c8e6 100644 --- a/spec/coffeescripts/jsx/gradebook/GradingPeriodCollectionSpec.coffee +++ b/spec/coffeescripts/jsx/gradebook/GradingPeriodCollectionSpec.coffee @@ -9,7 +9,6 @@ define [ ], (React, $, _, GradingPeriodCollection, fakeENV) -> TestUtils = React.addons.TestUtils - Simulate = TestUtils.Simulate module 'GradingPeriodCollection', setup: -> @@ -23,12 +22,22 @@ define [ @indexData = "grading_periods":[ { - "id":"1", "start_date":"2015-03-01T06:00:00Z", "end_date":"2015-05-31T05:00:00Z", - "weight":null, "title":"Spring", "permissions": { "update":true, "delete":true } + "id":"1", + "title":"Spring", + "start_date":"2015-03-01T06:00:00Z", + "end_date":"2015-05-31T05:00:00Z", + "close_date":"2015-06-07T05:00:00Z", + "weight":null, + "permissions": { "update":true, "delete":true } }, { - "id":"2", "start_date":"2015-06-01T05:00:00Z", "end_date":"2015-08-31T05:00:00Z", - "weight":null, "title":"Summer", "permissions": { "update":true, "delete":true } + "id":"2", + "title":"Summer", + "start_date":"2015-06-01T05:00:00Z", + "end_date":"2015-08-31T05:00:00Z", + "close_date":"2015-09-07T05:00:00Z", + "weight":null, + "permissions": { "update":true, "delete":true } } ] "grading_periods_read_only": false, @@ -38,12 +47,22 @@ define [ @formattedIndexData = "grading_periods":[ { - "id":"1", "startDate": new Date("2015-03-01T06:00:00Z"), "endDate": new Date("2015-05-31T05:00:00Z"), - "weight":null, "title":"Spring", "permissions": { "update":true, "delete":true } + "id":"1", + "title":"Spring", + "startDate": new Date("2015-03-01T06:00:00Z"), + "endDate": new Date("2015-05-31T05:00:00Z"), + "closeDate": new Date("2015-06-07T05:00:00Z"), + "weight":null, + "permissions": { "update":true, "delete":true } }, { - "id":"2", "startDate": new Date("2015-06-01T05:00:00Z"), "endDate": new Date("2015-08-31T05:00:00Z"), - "weight":null, "title":"Summer", "permissions": { "update":true, "delete":true } + "id":"2", + "title":"Summer", + "startDate": new Date("2015-06-01T05:00:00Z"), + "endDate": new Date("2015-08-31T05:00:00Z"), + "closeDate": new Date("2015-09-07T05:00:00Z"), + "weight":null, + "permissions": { "update":true, "delete":true } } ] "grading_periods_read_only": false, @@ -52,8 +71,13 @@ define [ @createdPeriodData = "grading_periods":[ { - "id":"3", "start_date":"2015-04-20T05:00:00Z", "end_date":"2015-04-21T05:00:00Z", - "weight":null, "title":"New Period!", "permissions": { "update":true, "delete":true } + "id":"3", + "title":"New Period!", + "start_date":"2015-04-20T05:00:00Z", + "end_date":"2015-04-21T05:00:00Z", + "close_date":"2015-04-28T05:00:00Z", + "weight":null, + "permissions": { "update":true, "delete":true } } ] @server.respondWith "GET", ENV.GRADING_PERIODS_URL, [200, {"Content-Type":"application/json"}, JSON.stringify @indexData] @@ -80,30 +104,9 @@ define [ equal @gradingPeriodCollection.refs.grading_period_1.props.readOnly, false equal @gradingPeriodCollection.refs.grading_period_2.props.readOnly, false - test 'createNewGradingPeriod adds a new period', -> - deepEqual @gradingPeriodCollection.state.periods.length, 2 - @gradingPeriodCollection.createNewGradingPeriod() - deepEqual @gradingPeriodCollection.state.periods.length, 3 - - test 'createNewGradingPeriod adds the new period with a blank title, start date, and end date', -> - @gradingPeriodCollection.createNewGradingPeriod() - newPeriod = _.find(@gradingPeriodCollection.state.periods, (p) => p.id.indexOf('new') > -1) - deepEqual newPeriod.title, '' - deepEqual newPeriod.startDate.getTime(), new Date('').getTime() - deepEqual newPeriod.endDate.getTime(), new Date('').getTime() - - test 'deleteGradingPeriod does not call confirmDelete if the grading period is not saved', -> - unsavedPeriod = [ - { - "id":"new1", "startDate": new Date("2029-03-01T06:00:00Z"), "endDate": new Date("2030-05-31T05:00:00Z"), - "weight":null, "title":"New Period. I'm not saved yet!", - "permissions": { "update":true, "delete":true } - } - ] - @gradingPeriodCollection.setState({periods: unsavedPeriod}) - confirmDelete = @stub($.fn, 'confirmDelete') - @gradingPeriodCollection.deleteGradingPeriod('new1') - ok confirmDelete.notCalled + test "renders grading periods with their individual 'closeDate'", -> + deepEqual @gradingPeriodCollection.refs.grading_period_1.props.closeDate, new Date("2015-06-07T05:00:00Z") + deepEqual @gradingPeriodCollection.refs.grading_period_2.props.closeDate, new Date("2015-09-07T05:00:00Z") test 'deleteGradingPeriod calls confirmDelete if the period being deleted is not new (it is saved server side)', -> confirmDelete = @stub($.fn, 'confirmDelete') @@ -238,13 +241,6 @@ define [ ok @gradingPeriodCollection.areDatesOverlapping(periodOne) ok @gradingPeriodCollection.areDatesOverlapping(periodTwo) - test 'renderAddPeriodButton does not render a button if canAddNewPeriods is false (based on permissions)', -> - @gradingPeriodCollection.setState({ canAddNewPeriods: false }) - notOk @gradingPeriodCollection.renderAddPeriodButton() - - test 'renderAddPeriodButton renders a button if canAddNewPeriods is true (based on permissions)', -> - ok @gradingPeriodCollection.renderAddPeriodButton() - test 'renderSaveButton does not render a button if the user cannot update any of the periods on the page', -> uneditable = [{ "id":"12", "startDate": new Date("2015-03-01T06:00:00Z"), "endDate": new Date("2015-05-31T05:00:00Z"), diff --git a/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee b/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee index 1a26e7142c6..ff6d615bbc6 100644 --- a/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee +++ b/spec/coffeescripts/jsx/gradebook/GradingPeriodSpec.coffee @@ -11,6 +11,7 @@ define [ ], (React, ReactDOM, $, _, GradingPeriod, fakeENV, DateHelper) -> TestUtils = React.addons.TestUtils + wrapper = document.getElementById('fixtures') module 'GradingPeriod', setup: -> @@ -20,25 +21,27 @@ define [ fakeENV.setup() ENV.GRADING_PERIODS_URL = "api/v1/courses/1/grading_periods" - @createdPeriodData = "grading_periods":[ - { - "id":"3", "start_date":"2015-04-20T05:00:00Z", "end_date":"2015-04-21T05:00:00Z", - "weight":null, "title":"New Period!", "permissions": { "update":true, "delete":true } - } - ] @updatedPeriodData = "grading_periods":[ { - "id":"1", "startDate":"2015-03-01T06:00:00Z", "endDate":"2015-05-31T05:00:00Z", - "weight":null, "title":"Updated Grading Period!", "permissions": { "update":true, "delete":true } + "id":"1", + "title":"Updated Grading Period!", + "startDate":"2015-03-01T06:00:00Z", + "endDate":"2015-05-31T05:00:00Z", + "closeDate":"2015-06-07T05:00:00Z", + "weight":null, + "permissions": { "update":true, "delete":true } } ] - @server.respondWith "POST", ENV.GRADING_PERIODS_URL, [200, {"Content-Type":"application/json"}, JSON.stringify @createdPeriodData] @server.respondWith "PUT", ENV.GRADING_PERIODS_URL + "/1", [200, {"Content-Type":"application/json"}, JSON.stringify @updatedPeriodData] - @props = + @server.respond() + + renderComponent: (opts = {}) -> + exampleProps = id: "1" title: "Spring" startDate: new Date("2015-03-01T00:00:00Z") endDate: new Date("2015-05-31T00:00:00Z") + closeDate: new Date("2015-06-07T00:00:00Z") weight: null disabled: false readOnly: false @@ -46,57 +49,64 @@ define [ onDeleteGradingPeriod: -> updateGradingPeriodCollection: sinon.spy() - @server.respond() - renderComponent: -> - GradingPeriodElement = React.createElement(GradingPeriod, @props) - @gradingPeriod = TestUtils.renderIntoDocument(GradingPeriodElement) + props = _.defaults(opts, exampleProps) + GradingPeriodElement = React.createElement(GradingPeriod, props) + ReactDOM.render(GradingPeriodElement, wrapper) + teardown: -> - ReactDOM.unmountComponentAtNode(ReactDOM.findDOMNode(@gradingPeriod).parentNode) + ReactDOM.unmountComponentAtNode(wrapper) ENV.GRADING_PERIODS_URL = null @server.restore() test 'sets initial state properly', -> - @renderComponent() - equal @gradingPeriod.state.title, @props.title - equal @gradingPeriod.state.startDate, @props.startDate - equal @gradingPeriod.state.endDate, @props.endDate - equal @gradingPeriod.state.weight, @props.weight + gradingPeriod = @renderComponent() + equal gradingPeriod.state.title, "Spring" + deepEqual gradingPeriod.state.startDate, new Date("2015-03-01T00:00:00Z") + deepEqual gradingPeriod.state.endDate, new Date("2015-05-31T00:00:00Z") + equal gradingPeriod.state.weight, null test 'onDateChange calls replaceInputWithDate', -> - @renderComponent() - replaceInputWithDate = @stub(@gradingPeriod, 'replaceInputWithDate') - @gradingPeriod.onDateChange("startDate", "period_start_date_1") + gradingPeriod = @renderComponent() + replaceInputWithDate = @stub(gradingPeriod, 'replaceInputWithDate') + gradingPeriod.onDateChange("startDate", "period_start_date_1") ok replaceInputWithDate.calledOnce test 'onDateChange calls updateGradingPeriodCollection', -> - @renderComponent() - @gradingPeriod.onDateChange("startDate", "period_start_date_1") - ok @gradingPeriod.props.updateGradingPeriodCollection.calledOnce + gradingPeriod = @renderComponent() + gradingPeriod.onDateChange("startDate", "period_start_date_1") + ok gradingPeriod.props.updateGradingPeriodCollection.calledOnce test 'onTitleChange changes the title state', -> - @renderComponent() + gradingPeriod = @renderComponent() fakeEvent = { target: { name: "title", value: "MXP: Most Xtreme Primate" } } - @gradingPeriod.onTitleChange(fakeEvent) - deepEqual @gradingPeriod.state.title, "MXP: Most Xtreme Primate" + gradingPeriod.onTitleChange(fakeEvent) + equal gradingPeriod.state.title, "MXP: Most Xtreme Primate" test 'onTitleChange calls updateGradingPeriodCollection', -> - @renderComponent() + gradingPeriod = @renderComponent() fakeEvent = { target: { name: "title", value: "MXP: Most Xtreme Primate" } } - @gradingPeriod.onTitleChange(fakeEvent) - ok @gradingPeriod.props.updateGradingPeriodCollection.calledOnce + gradingPeriod.onTitleChange(fakeEvent) + ok gradingPeriod.props.updateGradingPeriodCollection.calledOnce test 'replaceInputWithDate calls formatDatetimeForDisplay', -> - @renderComponent() + gradingPeriod = @renderComponent() formatDatetime = @stub(DateHelper, 'formatDatetimeForDisplay') fakeDateElement = { val: -> } - @gradingPeriod.replaceInputWithDate("startDate", fakeDateElement) + gradingPeriod.replaceInputWithDate("startDate", fakeDateElement) ok formatDatetime.calledOnce test "assigns the 'readOnly' property on the template when false", -> - @renderComponent() - equal @gradingPeriod.refs.template.props.readOnly, false + gradingPeriod = @renderComponent() + equal gradingPeriod.refs.template.props.readOnly, false test "assigns the 'readOnly' property on the template when true", -> - @props.readOnly = true - @renderComponent() - equal @gradingPeriod.refs.template.props.readOnly, true + gradingPeriod = @renderComponent(readOnly: true) + equal gradingPeriod.refs.template.props.readOnly, true + + test "assigns the 'closeDate' property", -> + gradingPeriod = @renderComponent() + deepEqual gradingPeriod.refs.template.props.closeDate, new Date("2015-06-07T00:00:00Z") + + test "assigns 'endDate' as 'closeDate' when 'closeDate' is not defined", -> + gradingPeriod = @renderComponent(closeDate: null) + deepEqual gradingPeriod.refs.template.props.closeDate, new Date("2015-05-31T00:00:00Z") diff --git a/spec/coffeescripts/jsx/gradebook/GradingPeriodTemplateSpec.coffee b/spec/coffeescripts/jsx/gradebook/GradingPeriodTemplateSpec.coffee index 2d768e06b05..6dace8fbff5 100644 --- a/spec/coffeescripts/jsx/gradebook/GradingPeriodTemplateSpec.coffee +++ b/spec/coffeescripts/jsx/gradebook/GradingPeriodTemplateSpec.coffee @@ -5,25 +5,35 @@ define [ 'jsx/grading/gradingPeriodTemplate' ], (React, ReactDOM, _, GradingPeriod) -> - TestUtils = React.addons.TestUtils + defaultProps = + title: "Spring" + startDate: new Date("2015-03-01T00:00:00Z") + endDate: new Date("2015-05-31T00:00:00Z") + closeDate: new Date("2015-06-07T00:00:00Z") + id: "1" + permissions: { + update: true + delete: true + } + disabled: false + readOnly: false + onDeleteGradingPeriod: -> + onDateChange: -> + onTitleChange: -> + + Simulate = React.addons.TestUtils.Simulate wrapper = document.getElementById('fixtures') module 'GradingPeriod with read-only permissions', - renderComponent: (opts) -> - defaultProps = - title: "Spring" - startDate: new Date("2015-03-01T00:00:00Z") - endDate: new Date("2015-05-31T00:00:00Z") - id: "1" - readOnly: false + renderComponent: (opts = {}) -> + readOnlyProps = permissions: { update: false delete: false } - onDeleteGradingPeriod: -> - @props = _.defaults(opts || {}, defaultProps) - GradingPeriodElement = React.createElement(GradingPeriod, @props) + props = _.defaults(opts, readOnlyProps, defaultProps) + GradingPeriodElement = React.createElement(GradingPeriod, props) ReactDOM.render(GradingPeriodElement, wrapper) teardown: -> @@ -41,52 +51,33 @@ define [ gradingPeriod = @renderComponent() notOk gradingPeriod.refs.deleteButton - test 'renderTitle returns a non-input element (since the grading period is readonly)', -> + test 'renders attributes as read-only', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderTitle().type, "input" + notEqual gradingPeriod.refs.title.type, "INPUT" + notEqual gradingPeriod.refs.startDate.type, "INPUT" + notEqual gradingPeriod.refs.endDate.type, "INPUT" - test 'renderStartDate returns a non-input element (since the grading period is readonly)', -> + test 'displays the correct attributes', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderStartDate().type, "input" + equal gradingPeriod.refs.title.textContent, "Spring" + equal gradingPeriod.refs.startDate.textContent, "Mar 1, 2015 at 12am" + equal gradingPeriod.refs.endDate.textContent, "May 31, 2015 at 12am" - test 'renderEndDate returns a non-input element (since the grading period is readonly)', -> + test 'displays the assigned close date', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderEndDate().type, "input" + equal gradingPeriod.refs.closeDate.textContent, "Jun 7, 2015 at 12am" - test 'displays the correct title', -> - gradingPeriod = @renderComponent() - titleNode = gradingPeriod.refs.title - equal titleNode.textContent, "Spring" - - test 'displays the correct start date', -> - gradingPeriod = @renderComponent() - startDateNode = gradingPeriod.refs.startDate - equal startDateNode.textContent, "Mar 1, 2015 at 12am" - - test 'displays the correct end date', -> - gradingPeriod = @renderComponent() - endDateNode = gradingPeriod.refs.endDate - equal endDateNode.textContent, "May 31, 2015 at 12am" + test 'uses the end date when close date is not defined', -> + gradingPeriod = @renderComponent(closeDate: null) + equal gradingPeriod.refs.closeDate.textContent, "May 31, 2015 at 12am" module "GradingPeriod with 'readOnly' set to true", - renderComponent: (opts) -> - defaultProps = - title: "Spring" - startDate: new Date("2015-03-01T00:00:00Z") - endDate: new Date("2015-05-31T00:00:00Z") - id: "1" + renderComponent: (opts = {}) -> + readOnlyProps = readOnly: true - permissions: { - update: true - delete: true - } - disabled: false - onDeleteGradingPeriod: -> - onDateChange: -> - onTitleChange: -> - @props = _.defaults(opts || {}, defaultProps) - GradingPeriodElement = React.createElement(GradingPeriod, @props) + props = _.defaults(opts, readOnlyProps, defaultProps) + GradingPeriodElement = React.createElement(GradingPeriod, props) ReactDOM.render(GradingPeriodElement, wrapper) teardown: -> @@ -104,108 +95,87 @@ define [ gradingPeriod = @renderComponent() notOk gradingPeriod.refs.deleteButton - test 'renderTitle returns a non-input element (since the grading period is readonly)', -> + test 'renders attributes as read-only', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderTitle().type, "input" + notEqual gradingPeriod.refs.title.type, "INPUT" + notEqual gradingPeriod.refs.startDate.type, "INPUT" + notEqual gradingPeriod.refs.endDate.type, "INPUT" - test 'renderStartDate returns a non-input element (since the grading period is readonly)', -> + test 'displays the correct attributes', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderStartDate().type, "input" + equal gradingPeriod.refs.title.textContent, "Spring" + equal gradingPeriod.refs.startDate.textContent, "Mar 1, 2015 at 12am" + equal gradingPeriod.refs.endDate.textContent, "May 31, 2015 at 12am" - test 'renderEndDate returns a non-input element (since the grading period is readonly)', -> + test 'displays the assigned close date', -> gradingPeriod = @renderComponent() - notEqual gradingPeriod.renderEndDate().type, "input" + equal gradingPeriod.refs.closeDate.textContent, "Jun 7, 2015 at 12am" - test 'displays the correct title', -> - gradingPeriod = @renderComponent() - titleNode = gradingPeriod.refs.title - equal titleNode.textContent, "Spring" - - test 'displays the correct start date', -> - gradingPeriod = @renderComponent() - startDateNode = gradingPeriod.refs.startDate - equal startDateNode.textContent, "Mar 1, 2015 at 12am" - - test 'displays the correct end date', -> - gradingPeriod = @renderComponent() - endDateNode = gradingPeriod.refs.endDate - equal endDateNode.textContent, "May 31, 2015 at 12am" + test 'uses the end date when close date is not defined', -> + gradingPeriod = @renderComponent(closeDate: null) + equal gradingPeriod.refs.closeDate.textContent, "May 31, 2015 at 12am" module 'editable GradingPeriod', - setup: -> - @props = - title: "Spring" - startDate: new Date("2015-03-01T00:00:00Z") - endDate: new Date("2015-05-31T00:00:00Z") - id: "1" - permissions: { - update: true - delete: true - } - disabled: false - readOnly: false - onDeleteGradingPeriod: -> - onDateChange: -> - onTitleChange: -> - - GradingPeriodElement = React.createElement(GradingPeriod, @props) - @gradingPeriod = ReactDOM.render(GradingPeriodElement, wrapper) + renderComponent: (opts = {}) -> + props = _.defaults(opts, defaultProps) + GradingPeriodElement = React.createElement(GradingPeriod, props) + ReactDOM.render(GradingPeriodElement, wrapper) teardown: -> ReactDOM.unmountComponentAtNode(wrapper) test 'renders a delete button', -> - ok @gradingPeriod.renderDeleteButton() + gradingPeriod = @renderComponent() + ok gradingPeriod.refs.deleteButton - test 'renderTitle returns an input element (since the grading period is editable)', -> - equal @gradingPeriod.renderTitle().type, "input" + test 'renders with input fields', -> + gradingPeriod = @renderComponent() + equal gradingPeriod.refs.title.tagName, "INPUT" + equal gradingPeriod.refs.startDate.tagName, "INPUT" + equal gradingPeriod.refs.endDate.tagName, "INPUT" - test 'renderStartDate returns an input element (since the grading period is editable)', -> - equal @gradingPeriod.renderStartDate().type, "input" + test 'displays the correct attributes', -> + gradingPeriod = @renderComponent() + equal gradingPeriod.refs.title.value, "Spring" + equal gradingPeriod.refs.startDate.value, "Mar 1, 2015 at 12am" + equal gradingPeriod.refs.endDate.value, "May 31, 2015 at 12am" - test 'renderEndDate returns an input element (since the grading period is editable)', -> - equal @gradingPeriod.renderEndDate().type, "input" + test 'uses the end date for close date', -> + gradingPeriod = @renderComponent() + equal gradingPeriod.refs.closeDate.textContent, "May 31, 2015 at 12am" - test 'displays the correct title', -> - titleNode = @gradingPeriod.refs.title - equal titleNode.value, "Spring" + test "calls onClick handler for clicks on 'delete grading period'", -> + deleteSpy = sinon.spy() + gradingPeriod = @renderComponent(onDeleteGradingPeriod: deleteSpy) + Simulate.click(gradingPeriod.refs.deleteButton) + ok deleteSpy.calledOnce - test 'displays the correct start date', -> - startDateNode = @gradingPeriod.refs.startDate - equal startDateNode.value, "Mar 1, 2015 at 12am" - - test 'displays the correct end date', -> - endDateNode = @gradingPeriod.refs.endDate - equal endDateNode.value, "May 31, 2015 at 12am" + test "ignores clicks on 'delete grading period' when disabled", -> + deleteSpy = sinon.spy() + gradingPeriod = @renderComponent(onDeleteGradingPeriod: deleteSpy, disabled: true) + Simulate.click(gradingPeriod.refs.deleteButton) + notOk deleteSpy.called module 'custom prop validation for editable periods', + renderComponent: (opts = {}) -> + props = _.defaults(opts, defaultProps) + GradingPeriodElement = React.createElement(GradingPeriod, props) + ReactDOM.render(GradingPeriodElement, wrapper) + setup: -> @consoleError = @stub(console, 'error') - @props = - title: "Spring" - startDate: new Date("2015-03-01T00:00:00Z") - endDate: new Date("2015-05-31T00:00:00Z") - id: "1" - permissions: { - update: true - delete: true - } - disabled: false - readOnly: false - onDeleteGradingPeriod: -> - onDateChange: -> - onTitleChange: -> + + teardown: -> + ReactDOM.unmountComponentAtNode(wrapper) test 'does not warn of invalid props if all required props are present and of the correct type', -> - React.createElement(GradingPeriod, @props) + @renderComponent() ok @consoleError.notCalled test 'warns if required props are missing', -> - delete @props.disabled - React.createElement(GradingPeriod, @props) + @renderComponent(disabled: null) ok @consoleError.calledOnce test 'warns if required props are of the wrong type', -> - @props.onDeleteGradingPeriod = "a/s/l?" - React.createElement(GradingPeriod, @props) + @renderComponent(onDeleteGradingPeriod: "invalid-type") ok @consoleError.calledOnce diff --git a/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx b/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx index 988862c2f0b..3ecdda6519f 100644 --- a/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx +++ b/spec/javascripts/jsx/grading/AccountGradingPeriodSpec.jsx @@ -16,7 +16,8 @@ define([ id: "1", title: "We did it! We did it! We did it! #dora #boots", startDate: new Date("2015-01-01T20:11:00+00:00"), - endDate: new Date("2015-03-01T00:00:00+00:00") + endDate: new Date("2015-03-01T00:00:00+00:00"), + closeDate: new Date("2015-03-08T00:00:00+00:00") }, readOnly: false, onEdit: () => {}, @@ -75,6 +76,12 @@ define([ equal(endDate, "End Date: Mar 1, 2015 at 12am"); }); + test("displays the close date in a friendly format", function() { + let period = this.renderComponent(); + const closeDate = ReactDOM.findDOMNode(period.refs.closeDate).textContent; + equal(closeDate, "Close Date: Mar 8, 2015 at 12am"); + }); + test("calls the 'onEdit' callback when the edit button is clicked", function() { let spy = sinon.spy(); let period = this.renderComponent({onEdit: spy}); diff --git a/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx index e619c88ba5a..0402b7828d6 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodFormSpec.jsx @@ -7,9 +7,18 @@ define([ const wrapper = document.getElementById('fixtures'); const Simulate = React.addons.TestUtils.Simulate; + const examplePeriod = { + id: '1', + title: 'Q1', + startDate: new Date("2015-11-01T12:00:00Z"), + endDate: new Date("2015-12-31T12:00:00Z"), + closeDate: new Date("2016-01-07T12:00:00Z") + }; + module('GradingPeriodForm', { renderComponent: function(opts={}) { const defaults = { + period: examplePeriod, disabled: false, onSave: () => {}, onCancel: () => {} @@ -23,43 +32,112 @@ define([ } }); - test('mounts', function() { + test("sets form values from 'period' props", function() { let form = this.renderComponent(); - ok(form.isMounted()); + equal(form.refs.title.value, 'Q1'); + equal(form.refs.startDate.refs.dateInput.value, 'Nov 1, 2015 at 12pm'); + equal(form.refs.endDate.refs.dateInput.value, 'Dec 31, 2015 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Jan 7 at 12pm'); }); test('renders with the save button enabled', function() { let form = this.renderComponent(); - let saveButton = React.findDOMNode(form.refs.saveButton); + let saveButton = ReactDOM.findDOMNode(form.refs.saveButton); equal(saveButton.disabled, false); }); test('renders with the cancel button enabled', function() { let form = this.renderComponent(); - let cancelButton = React.findDOMNode(form.refs.saveButton); + let cancelButton = ReactDOM.findDOMNode(form.refs.saveButton); equal(cancelButton.disabled, false); }); test('optionally renders with the save and cancel buttons disabled', function() { let form = this.renderComponent({disabled: true}); - let saveButton = React.findDOMNode(form.refs.saveButton); - let cancelButton = React.findDOMNode(form.refs.cancelButton); + let saveButton = ReactDOM.findDOMNode(form.refs.saveButton); + let cancelButton = ReactDOM.findDOMNode(form.refs.cancelButton); equal(saveButton.disabled, true); equal(cancelButton.disabled, true); }); + test("auto-updates 'closeDate' when not already set and 'endDate' changes", function() { + let incompletePeriod = _.extend({}, examplePeriod, { closeDate: null }); + let form = this.renderComponent({period: incompletePeriod}); + let endDateInput = ReactDOM.findDOMNode(form.refs.endDate.refs.dateInput); + endDateInput.value = 'Dec 31, 2015 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + equal(form.refs.endDate.refs.dateInput.value, 'Dec 31, 2015 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Dec 31, 2015 at 12pm'); + }); + + test("auto-updates 'closeDate' when set equal to 'endDate' and 'endDate' changes", function() { + let consistentPeriod = _.extend({}, examplePeriod, { closeDate: examplePeriod.endDate }); + let form = this.renderComponent({period: consistentPeriod}); + let endDateInput = ReactDOM.findDOMNode(form.refs.endDate.refs.dateInput); + endDateInput.value = 'Dec 30, 2015 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + equal(form.refs.endDate.refs.dateInput.value, 'Dec 30, 2015 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Dec 30, 2015 at 12pm'); + }); + + test("preserves 'closeDate' when not set equal to 'endDate' and 'endDate' changes", function() { + let form = this.renderComponent(); + let endDateInput = ReactDOM.findDOMNode(form.refs.endDate.refs.dateInput); + endDateInput.value = 'Dec 30, 2015 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + equal(form.refs.endDate.refs.dateInput.value, 'Dec 30, 2015 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Jan 7 at 12pm'); + }); + + test("preserves 'closeDate' when already set and 'endDate' changes to match, then changes again", function() { + let form = this.renderComponent(); + let endDateInput = ReactDOM.findDOMNode(form.refs.endDate.refs.dateInput); + endDateInput.value = 'Jan 7 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + endDateInput.value = 'Dec 30, 2015 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + equal(form.refs.endDate.refs.dateInput.value, 'Dec 30, 2015 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Jan 7 at 12pm'); + }); + + test("auto-updates 'closeDate' when cleared and 'endDate' changes", function() { + let form = this.renderComponent(); + let closeDateInput = ReactDOM.findDOMNode(form.refs.closeDate.refs.dateInput); + closeDateInput.value = ''; + closeDateInput.dispatchEvent(new Event("change")); + let endDateInput = ReactDOM.findDOMNode(form.refs.endDate.refs.dateInput); + endDateInput.value = 'Jan 7 at 12pm'; + endDateInput.dispatchEvent(new Event("change")); + equal(form.refs.endDate.refs.dateInput.value, 'Jan 7 at 12pm'); + equal(form.refs.closeDate.refs.dateInput.value, 'Jan 7 at 12pm'); + }); + test("calls the 'onSave' callback when the save button is clicked", function() { let spy = sinon.spy(); let form = this.renderComponent({onSave: spy}); - let saveButton = React.findDOMNode(form.refs.saveButton); + let saveButton = ReactDOM.findDOMNode(form.refs.saveButton); Simulate.click(saveButton); ok(spy.calledOnce); }); + test("sends form values in 'onSave'", function() { + let spy = sinon.spy(); + let form = this.renderComponent({onSave: spy}); + let saveButton = ReactDOM.findDOMNode(form.refs.saveButton); + Simulate.click(saveButton); + deepEqual(spy.args[0][0], { + id: '1', + title: 'Q1', + startDate: new Date("2015-11-01T12:00:00Z"), + endDate: new Date("2015-12-31T12:00:00Z"), + closeDate: new Date("2016-01-07T12:00:00Z") + }); + }); + test("calls the 'onCancel' callback when the cancel button is clicked", function() { let spy = sinon.spy(); let form = this.renderComponent({onCancel: spy}); - let cancelButton = React.findDOMNode(form.refs.cancelButton); + let cancelButton = ReactDOM.findDOMNode(form.refs.cancelButton); Simulate.click(cancelButton); ok(spy.calledOnce); }); diff --git a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx index ace6f8cc540..84756d4632f 100644 --- a/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx +++ b/spec/javascripts/jsx/grading/GradingPeriodSetSpec.jsx @@ -41,19 +41,20 @@ define([ id: "1", title: "We did it! We did it! We did it! #dora #boots", startDate: new Date("2015-01-01T20:11:00+00:00"), - endDate: new Date("2015-03-01T00:00:00+00:00") - }, - { + endDate: new Date("2015-03-01T00:00:00+00:00"), + closeDate: new Date("2015-03-01T00:00:00+00:00") + },{ id: "3", title: "Como estas?", startDate: new Date("2014-11-01T20:11:00+00:00"), - endDate: new Date("2014-11-11T00:00:00+00:00") - }, - { + endDate: new Date("2014-11-11T00:00:00+00:00"), + closeDate: new Date("2014-11-11T00:00:00+00:00") + },{ id: "2", title: "Swiper no swiping!", startDate: new Date("2015-04-01T20:11:00+00:00"), - endDate: new Date("2015-05-01T00:00:00+00:00") + endDate: new Date("2015-05-01T00:00:00+00:00"), + closeDate: new Date("2015-05-01T00:00:00+00:00") } ]; @@ -61,7 +62,8 @@ define([ id: "4", title: "Example Period", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; const props = { @@ -383,7 +385,8 @@ define([ id: "1", title: "", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -397,7 +400,8 @@ define([ id: "1", title: " ", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -410,7 +414,8 @@ define([ let period = { title: "Period without Start Date", startDate: undefined, - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -423,7 +428,22 @@ define([ let period = { title: "Period without End Date", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: null + endDate: null, + closeDate: new Date("2015-03-03T00:00:00+00:00") + }; + let update = this.stubUpdate(); + let set = this.renderComponent(); + this.callOnSave(set, period); + notOk(gradingPeriodsApi.batchUpdate.called, "does not call update"); + ok(set.refs.editPeriodForm, "form is still visible"); + }); + + test('does not save a grading period without a valid closeDate', function() { + let period = { + title: "Period without End Date", + startDate: new Date("2015-03-02T20:11:00+00:00"), + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: null }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -436,7 +456,8 @@ define([ let period = { title: "Period with Overlapping Start Date", startDate: new Date("2015-04-30T20:11:00+00:00"), - endDate: new Date("2015-05-30T00:00:00+00:00") + endDate: new Date("2015-05-30T00:00:00+00:00"), + closeDate: new Date("2015-05-30T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -449,7 +470,8 @@ define([ let period = { title: "Period with Overlapping End Date", startDate: new Date("2014-12-30T20:11:00+00:00"), - endDate: new Date("2015-01-30T00:00:00+00:00") + endDate: new Date("2015-01-30T00:00:00+00:00"), + closeDate: new Date("2015-01-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -462,7 +484,22 @@ define([ let period = { title: "Overlapping Period", startDate: new Date("2015-03-03T00:00:00+00:00"), - endDate: new Date("2015-03-02T20:11:00+00:00") + endDate: new Date("2015-03-02T20:11:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") + }; + let update = this.stubUpdate(); + let set = this.renderComponent(); + this.callOnSave(set, period); + notOk(gradingPeriodsApi.batchUpdate.called, "does not call update"); + ok(set.refs.editPeriodForm, "form is still visible"); + }); + + test('does not save a grading period with closeDate before endDate', function() { + let period = { + title: "Overlapping Period", + startDate: new Date("2015-03-01T00:00:00+00:00"), + endDate: new Date("2015-03-02T20:11:00+00:00"), + closeDate: new Date("2015-03-02T20:10:59+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -677,7 +714,8 @@ define([ let period = { title: "", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -690,7 +728,8 @@ define([ let period = { title: "Period without Start Date", startDate: undefined, - endDate: new Date("2015-03-03T00:00:00+00:00") + endDate: new Date("2015-03-03T00:00:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -703,7 +742,8 @@ define([ let period = { title: "Period without End Date", startDate: new Date("2015-03-02T20:11:00+00:00"), - endDate: null + endDate: null, + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -716,7 +756,8 @@ define([ let period = { title: "Period with Overlapping Start Date", startDate: new Date("2015-04-30T20:11:00+00:00"), - endDate: new Date("2015-05-30T00:00:00+00:00") + endDate: new Date("2015-05-30T00:00:00+00:00"), + closeDate: new Date("2015-05-30T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -729,7 +770,8 @@ define([ let period = { title: "Period with Overlapping End Date", startDate: new Date("2014-12-30T20:11:00+00:00"), - endDate: new Date("2015-01-30T00:00:00+00:00") + endDate: new Date("2015-01-30T00:00:00+00:00"), + closeDate: new Date("2015-01-30T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); @@ -742,7 +784,8 @@ define([ let period = { title: "Overlapping Period", startDate: new Date("2015-03-03T00:00:00+00:00"), - endDate: new Date("2015-03-02T20:11:00+00:00") + endDate: new Date("2015-03-02T20:11:00+00:00"), + closeDate: new Date("2015-03-03T00:00:00+00:00") }; let update = this.stubUpdate(); let set = this.renderComponent(); diff --git a/spec/lib/data_fixup/populate_grading_period_close_dates_spec.rb b/spec/lib/data_fixup/populate_grading_period_close_dates_spec.rb new file mode 100644 index 00000000000..a73f163672c --- /dev/null +++ b/spec/lib/data_fixup/populate_grading_period_close_dates_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe DataFixup::PopulateGradingPeriodCloseDates do + before(:each) do + root_account = Account.create(name: 'new account') + group = Factories::GradingPeriodGroupHelper.new.create_for_account(root_account) + period_helper = Factories::GradingPeriodHelper.new + @first_period = period_helper.create_presets_for_group(group, :past).first + @first_period.close_date = nil + @first_period.save! + @second_period = period_helper.create_presets_for_group(group, :current).first + @second_period.close_date = 3.days.from_now(@second_period.end_date) + @second_period.save! + end + + before(:each) do + DataFixup::PopulateGradingPeriodCloseDates.run + end + + it "does not alter already-set close dates" do + @second_period.reload + expect(@second_period.close_date).to eq 3.days.from_now(@second_period.end_date) + end + + it "sets the close date to the end date for periods with nil close dates" do + @first_period.reload + expect(@first_period.close_date).to eq @first_period.end_date + end +end diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index bd31e9b614f..822e2956318 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -60,26 +60,50 @@ describe GradingPeriod do end describe "close_date" do - it "sets the close_date to the end_date if no close_date is provided" do - grading_period = grading_period_group.grading_periods.create!(params.except(:close_date)) - expect(grading_period.close_date).to eq(grading_period.end_date) + context "grading period group belonging to an account" do + it "allows setting a close_date that is different from the end_date" do + grading_period = grading_period_group.grading_periods.create!(params) + expect(grading_period.close_date).not_to eq(grading_period.end_date) + end + + it "sets the close_date to the end_date if no close_date is provided" do + grading_period = grading_period_group.grading_periods.create!(params.except(:close_date)) + expect(grading_period.close_date).to eq(grading_period.end_date) + end + + it "considers the grading period invalid if the close date is before the end date" do + period_params = params.merge(close_date: 1.day.ago(params[:end_date])) + grading_period = grading_period_group.grading_periods.build(period_params) + expect(grading_period).to be_invalid + end + + it "considers the grading period valid if the close date is equal to the end date" do + period_params = params.merge(close_date: params[:end_date]) + grading_period = grading_period_group.grading_periods.build(period_params) + expect(grading_period).to be_valid + end end - it "allows setting a close_date that is different from the end_date" do - grading_period = grading_period_group.grading_periods.create!(params) - expect(grading_period.close_date).not_to eq(grading_period.end_date) - end + context "grading period group belonging to a course" do + let(:course_grading_period_group) { group_helper.legacy_create_for_course(course) } - it "considers the grading period invalid if the close date is before the end date" do - period_params = params.merge(close_date: 1.day.ago(params[:end_date])) - grading_period = grading_period_group.grading_periods.build(period_params) - expect(grading_period).to be_invalid - end + it "does not allow setting a close_date that is different from the end_date" do + grading_period = course_grading_period_group.grading_periods.create!(params) + expect(grading_period.close_date).to eq(params[:end_date]) + end - it "considers the grading period valid if the close date is equal to the end date" do - period_params = params.merge(close_date: params[:end_date]) - grading_period = grading_period_group.grading_periods.build(period_params) - expect(grading_period).to be_valid + it "sets the close_date to the end_date if no close_date is provided" do + grading_period = course_grading_period_group.grading_periods.create!(params.except(:close_date)) + expect(grading_period.close_date).to eq(grading_period.end_date) + end + + it "sets the close_date to the end_date when the grading period is updated" do + grading_period = course_grading_period_group.grading_periods.create!(params.except(:close_date)) + new_end_date = 5.weeks.from_now(now) + grading_period.end_date = new_end_date + grading_period.save! + expect(grading_period.close_date).to eq(new_end_date) + end end end diff --git a/spec/selenium/grading_periods_course_spec.rb b/spec/selenium/grading_periods_course_spec.rb index 7ab94ea4e35..f533b119e40 100644 --- a/spec/selenium/grading_periods_course_spec.rb +++ b/spec/selenium/grading_periods_course_spec.rb @@ -50,11 +50,6 @@ describe 'Course Grading Periods' do expect(ff(grading_period_selector).length).to be 1 end - it 'does not allow adding grading periods', priority: "1", test_id: 239999 do - get "/courses/#{@course.id}/grading_standards" - expect(f('#grading_periods')).to_not contain_css(('#add-period-button')) - end - it 'allows updating grading periods', priority: "1", test_id: 202317 do period_helper.create_with_group_for_course(@course) get "/courses/#{@course.id}/grading_standards" diff --git a/spec/selenium/multiple_grading_periods_spec.rb b/spec/selenium/multiple_grading_periods_spec.rb index 10a3f848cbd..8a20ef50571 100644 --- a/spec/selenium/multiple_grading_periods_spec.rb +++ b/spec/selenium/multiple_grading_periods_spec.rb @@ -131,40 +131,6 @@ describe "interaction with multiple grading periods" do end end - context 'sub-accounts' do - # top-level account & grading periods setup - let(:parent_account) { Account.default } - let!(:enable_mgp_flag) { parent_account.enable_feature!(:multiple_grading_periods) } - # sub-account & grading periods setup - let(:sub_account) { Account.create(name: 'Sub Account', parent_account: parent_account) } - # sub-account course setup - let(:sub_account_course) do - sub_account.courses.create( - name: 'Sub-Account Course', - workflow_state: 'active' - ) - end - let(:sub_account_teacher) { user(active_all: true) } - let(:enroll_teacher) do - sub_account_course.enroll_user( - sub_account_teacher, - 'TeacherEnrollment', - enrollment_state: 'active' - ) - end - let(:view_sub_course_grading_period) do - sub_account_course - enroll_teacher - user_session(sub_account_teacher) - get "/courses/#{sub_account_course.id}/grading_standards" - end - - it 'does not allow creation of a GP in sub-account course', priority: "1", test_id: 587759 do - view_sub_course_grading_period - expect(f('#grading_periods')).not_to contain_css('#add-period-button') - end - end - context 'student view' do let(:account) { Account.default } let(:test_course) { account.courses.create!(name: 'New Course') } From 4d4a6827718676f5bb345402c10fbef0bc6aee60 Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Mon, 25 Jul 2016 17:33:19 -0500 Subject: [PATCH 03/10] lock cells based on close date in gradebook in the gradebook, cells will now be locked down if the due date for the submission cell falls in a grading period that is closed. a grading period is closed when today's date is past the close_date attribute on the grading period. closes CNVS-26717 test plan: - enable multiple grading periods at the account-level - we'll be testing a course under the account mentioned above. ensure the course does not have any course grading periods set up. - run the provided seeds file to set up grading periods on the account. change the account_id in the seeds file if needed (it defaults to account id 1) $ bundle exec rake db:seed - visit the account grading standards page and verify the sample grading period set and grading periods were created. the set name should be "test CNVS-26717". associate the set with the enrollment term that the course you'll be testing belongs to, and save the set. - go to the course grading standards page and verify the sample grading periods show up (the ones we just set up). - log in as a non-admin teacher in the course - create a section (Section 1) and a group (Group 1) in the course - create the following four students in your course: * Student A - belongs to Section 1 * Student B - belongs to Group 1 * Student C * Student D - belongs to Section 1 - belongs to Group 1 - create the following assignments in the course: * Assignment 1 - due for Student A 3 days ago - due for 'Everyone Else' 42 days ago * Assignment 2 - due for Student D 42 days ago - due for Section 1 4 days ago - due for Group 1 with no due date * Assignment 3 - due for Group 1 42 days from now - due for 'Everyone Else' 4 days from now - go to the gradebook. select 'All Grading Periods' and verify the following: * Assignment 1 - the cell for Student A is unlocked - the cell for Student B, C, and D is locked and the tooltip reads "this submission falls in a closed grading period" * Assignment 2 - the cell for Student A, B, and D is unlocked - the cell for Student C is locked with no tooltip message * Assignment 3 - the cell for Student A, B, C, and D is unlocked - select "closed GP" and verify the following: * Assignment 1 - the cell for Student A is locked and the tooltip reads "this submission is in another grading period" - the cell for Student B, C, and D is locked and the tooltip reads "this submission is due in a closed grading period" * Assignment 2 - the cell for Student A, B, and D is locked and the tooltip reads "this submission is in another grading period" - the cell for Student C is locked with no tooltip message * Assignment 3 - not shown - select "GP start 40 days ago, end 2 days ago, close 2 days from now" and verify the following: * Assignment 1 - the cell for Student A is unlocked - the cell for Student B, C, and D is locked and the tooltip reads "this submission is in another grading period" * Assignment 2 - the cell for Student A is unlocked - the cell for Student B and D is locked and the tooltip reads "this submission is in another grading period" - the cell for Student C is locked with no tooltip message * Assignment 3 - not shown - select "GP start 2 days ago, end 2 days from now, close 1 month from now" and verify the following: * Assignment 1 - not shown * Assignment 2 - not shown * Assignment 3 - not shown - select "GP start 2 days from now, end 1 month from now, close 2 months from now" and verify the following: * Assignment 1 - not shown * Assignment 2 - the cell for Student A is locked and the tooltip reads "this submission is in another grading period" - the cell for Student B and D is unlocked - the cell for Student C is locked with no tooltip message * Assignment 3 - the cell for Student A and C is unlocked - the cell for Student B and D is locked and the tooltip reads "this submission does not fall in any grading period" - log in as an admin. verify the cells that were locked due to "this submission falls in a closed grading period" are now unlocked - edge case: create an assignment and assign it to Student A and Student B today. go to the gradebook and give Student A and Student B a grade. Next, edit the assignment and make it _only_ assigned to Student B (remove the assignment to Student A). Go back to the gradebook and verify you can still change Student A's grade BUT if you change Student A's grade to nothing, the cell should lock. - turn off Multiple Grading Periods for the course. verify that no cells are locked due to grading period reasons. Change-Id: I9803f1ab484ac415c2d4e0d8ad1c278952bf5849 Reviewed-on: https://gerrit.instructure.com/87986 Tested-by: Jenkins Reviewed-by: Jeremy Neander QA-Review: KC Naegle Reviewed-by: Derek Bender Product-Review: Keith T. Garner --- app/coffeescripts/gradebook2/Gradebook.coffee | 203 +--- .../gradebook2/GradebookTranslations.coffee | 3 + .../gradebook2/SubmissionCell.coffee | 31 +- .../AccountsTreeStore.jsx | 3 +- app/jsx/gradebook/SubmissionStateMap.jsx | 184 +++ app/models/grading_period.rb | 2 +- .../gradebook2/GradebookSpec.coffee | 509 +------- .../gradebook2/SubmissionCellSpec.coffee | 86 +- .../SubmissionStateMapGradeVisibilitySpec.jsx | 679 +++++++++++ .../SubmissionStateMapLockingSpec.jsx | 919 ++++++++++++++ .../SubmissionStateMapTooltipSpec.jsx | 1069 +++++++++++++++++ spec/models/grading_period_spec.rb | 13 +- 12 files changed, 3018 insertions(+), 683 deletions(-) create mode 100644 app/jsx/gradebook/SubmissionStateMap.jsx create mode 100644 spec/javascripts/jsx/gradebook/SubmissionStateMapGradeVisibilitySpec.jsx create mode 100644 spec/javascripts/jsx/gradebook/SubmissionStateMapLockingSpec.jsx create mode 100644 spec/javascripts/jsx/gradebook/SubmissionStateMapTooltipSpec.jsx diff --git a/app/coffeescripts/gradebook2/Gradebook.coffee b/app/coffeescripts/gradebook2/Gradebook.coffee index a1400f446d2..c16b2af3e03 100644 --- a/app/coffeescripts/gradebook2/Gradebook.coffee +++ b/app/coffeescripts/gradebook2/Gradebook.coffee @@ -28,6 +28,7 @@ define [ 'str/htmlEscape' 'jsx/gradebook/SISGradePassback/PostGradesStore' 'jsx/gradebook/SISGradePassback/PostGradesApp' + 'jsx/gradebook/SubmissionStateMap' 'jst/gradebook2/column_header' 'jst/gradebook2/group_total_cell' 'jst/gradebook2/row_student_name' @@ -56,7 +57,7 @@ InputFilterView, I18n, GRADEBOOK_TRANSLATIONS, GradeCalculator, UserSettings, Spinner, SubmissionDetailsDialog, AssignmentGroupWeightsDialog, GradeDisplayWarningDialog, PostGradesFrameDialog, SubmissionCell, GradebookHeaderMenu, NumberCompare, htmlEscape, PostGradesStore, PostGradesApp, -ColumnHeaderTemplate, GroupTotalCellTemplate, RowStudentNameTemplate, +SubmissionStateMap, ColumnHeaderTemplate, GroupTotalCellTemplate, RowStudentNameTemplate, SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> class Gradebook @@ -94,10 +95,15 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> @options.settings['show_inactive_enrollments'] == "true" @totalColumnInFront = UserSettings.contextGet 'total_column_in_front' @numberOfFrozenCols = if @totalColumnInFront then 3 else 2 - @mgpEnabled = @options.multiple_grading_periods_enabled - @gradingPeriods = @options.active_grading_periods - @indexedGradingPeriods = _.indexBy @gradingPeriods, 'id' + @gradingPeriodsEnabled = @options.multiple_grading_periods_enabled + @gradingPeriods = _.map @options.active_grading_periods, (gradingPeriod) => + _.extend({}, gradingPeriod, closed: @gradingPeriodIsClosed(gradingPeriod)) @gradingPeriodToShow = @getGradingPeriodToShow() + @submissionStateMap = new SubmissionStateMap + gradingPeriodsEnabled: @gradingPeriodsEnabled + selectedGradingPeriodID: @gradingPeriodToShow + gradingPeriods: @gradingPeriods + isAdmin: _.contains(ENV.current_user_roles, "admin") @gradebookColumnSizeSettings = @options.gradebook_column_size_settings @gradebookColumnOrderSettings = @options.gradebook_column_order_settings @teacherNotesNotYetLoaded = !@options.teacher_notes? || @options.teacher_notes.hidden @@ -109,7 +115,7 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> $.subscribe 'currentGradingPeriod/change', @updateCurrentGradingPeriod assignmentGroupsParams = { exclude_response_fields: @fieldsToExcludeFromAssignments } - if @mgpEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != '' + if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != '' $.extend(assignmentGroupsParams, {grading_period_id: @gradingPeriodToShow}) $('li.external-tools-dialog > a[data-url], button.external-tools-dialog').on 'click keyclick', (event) -> @@ -122,7 +128,7 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> submissionParams = response_fields: ['id', 'user_id', 'url', 'score', 'grade', 'submission_type', 'submitted_at', 'assignment_id', 'grade_matches_current_submission', 'attachments', 'late', 'workflow_state', 'excused'] exclude_response_fields: ['preview_url'] - submissionParams['grading_period_id'] = @gradingPeriodToShow if @mgpEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != '' + submissionParams['grading_period_id'] = @gradingPeriodToShow if @gradingPeriodsEnabled && @gradingPeriodToShow && @gradingPeriodToShow != '0' && @gradingPeriodToShow != '' dataLoader = DataLoader.loadGradebookData( assignmentGroupsURL: @options.assignment_groups_url assignmentGroupsParams: assignmentGroupsParams @@ -188,23 +194,8 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> filteredVisibility = assignment.assignment_visibility.filter (id) -> id != hiddenSub.user_id assignment.assignment_visibility = filteredVisibility - # dependencies - assignmentGroupsLoaded - disableAssignmentsInClosedGradingPeriods: () -> - closedAdminGradingPeriods = @getClosedAdminGradingPeriods() - - if closedAdminGradingPeriods.length > 0 - assignments = @getAssignmentsInClosedGradingPeriods() - @disabledAssignments = assignments.map (a) -> a.id - - getClosedAdminGradingPeriods: () -> - _.select @gradingPeriods, (gradingPeriod) => - @gradingPeriodIsAdmin(gradingPeriod) && @gradingPeriodIsClosed(gradingPeriod) - - gradingPeriodIsAdmin: (gradingPeriod) -> - !gradingPeriod.permissions.update - gradingPeriodIsClosed: (gradingPeriod) -> - new Date(gradingPeriod.end_date) < new Date() + new Date(gradingPeriod.close_date) < new Date() gradingPeriodIsActive: (gradingPeriodId) -> activePeriodIds = _.pluck(@gradingPeriods, 'id') @@ -217,18 +208,6 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> else @options.current_grading_period_id - getAssignmentsInClosedGradingPeriods: () -> - latestEndDate = new Date(@options.latest_end_date_of_admin_created_grading_periods_in_the_past) - #return assignments whose end date is within the latest closed's end date - _.select @assignments, (a) => - @assignmentIsDueBeforeEndDate(a, latestEndDate) - - assignmentIsDueBeforeEndDate: (assignment, gradingPeriodEndDate) -> - if assignment.due_at - new Date(assignment.due_at) <= gradingPeriodEndDate - else - false - onShow: -> $(".post-grades-button-placeholder").show() return if @startedInitializing @@ -275,21 +254,6 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> @assignments[assignment.id] = assignment @postGradesStore.setGradeBookAssignments @assignments - @disableAssignmentsInClosedGradingPeriods() if @mgpEnabled - - initializeSubmissionsForStudent: (student) => - for assignment_id, assignment of @assignments - student["assignment_#{assignment_id}"] ?= { assignment_id: assignment_id, user_id: student.id } - submission = student["assignment_#{assignment_id}"] - - if @submissionOutsideOfGradingPeriod(submission, student) - submission.hidden = true - submission.outsideOfGradingPeriod = true - - student.initialized = true - @calculateStudentGrade(student) - @grid?.invalidateRow(student.row) - gotSections: (sections) => @sections = {} for section in sections @@ -322,10 +286,15 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> e.type == "StudentEnrollment" || e.type == "StudentViewEnrollment" setupGrading: (students) => - # fill in dummy submissions, so there's something there even if the - # student didn't submit anything for that assignment + @submissionStateMap.setup(students, @assignments) for student in students - @initializeSubmissionsForStudent(student) + for assignment_id of @assignments + student["assignment_#{assignment_id}"] ?= + @submissionStateMap.getSubmission student.id, assignment_id + + student.initialized = true + @calculateStudentGrade(student) + @grid?.invalidateRow(student.row) @setAssignmentVisibility(_.pluck(students, 'id')) @@ -634,17 +603,9 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> activeCell.row is student.row and activeCell.cell is cell #check for DA visible - if submission.assignment_visible? - submission.hidden = !submission.assignment_visible - - if @submissionOutsideOfGradingPeriod(submission, student) - submission.hidden = true - submission.outsideOfGradingPeriod = true - - if submission.hidden - @updateAssignmentVisibilities(submission) - + @updateAssignmentVisibilities(submission) unless submission.assignment_visible @updateSubmission(submission) + @submissionStateMap.setSubmissionCellState(student, @assignments[submission.assignment_id], submission) @calculateStudentGrade(student) @grid.updateCell student.row, cell unless thisCellIsActive @updateRowTotals student.row @@ -658,113 +619,39 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> if !@rows[row].loaded or !@rows[row].initialized @staticCellFormatter(row, col, '') else - if submission.outsideOfGradingPeriod - @uneditableCellOutsideOfGradingPeriodFormatter(row, col) - else if submission.hidden - @uneditableCellFormatter(row, col) - else if !submission? - @staticCellFormatter(row, col, '-') + cellAttributes = @submissionStateMap.getSubmissionState(submission) + if cellAttributes.hideGrade + @lockedAndHiddenGradeCellFormatter(row, col, cellAttributes.tooltip) else assignment = @assignments[submission.assignment_id] student = @students[submission.user_id] + formatterOpts = + isLocked: cellAttributes.locked + tooltip: cellAttributes.tooltip if !assignment? @staticCellFormatter(row, col, '') else if submission.workflow_state == 'pending_review' - (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student) + (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student, formatterOpts) else if assignment.grading_type == 'points' && assignment.points_possible - SubmissionCell.out_of.formatter(row, col, submission, assignment, student) + SubmissionCell.out_of.formatter(row, col, submission, assignment, student, formatterOpts) else - (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student) - - indexedOverrides: => - @_indexedOverrides ||= (=> - indexed = { - studentOverrides: {}, - groupOverrides: {}, - sectionOverrides: {} - } - - _.each @assignments, (assignment) -> - if assignment.has_overrides && assignment.overrides - _.each assignment.overrides, (override) -> - if override.student_ids - indexed.studentOverrides[assignment.id] ?= {} - _.each override.student_ids, (studentId) -> - indexed.studentOverrides[assignment.id][studentId] = override - else if sectionId = override.course_section_id - indexed.sectionOverrides[assignment.id] ?= {} - indexed.sectionOverrides[assignment.id][sectionId] = override - else if groupId = override.group_id - indexed.groupOverrides[assignment.id] ?= {} - indexed.groupOverrides[assignment.id][groupId] = override - - indexed - )() - - # depedencies: assignmentGroupsLoaded - submissionOutsideOfGradingPeriod: (submission, student) -> - return false unless @mgpEnabled - selectedPeriodId = @gradingPeriodToShow - return false if @isAllGradingPeriods(selectedPeriodId) - - assignment = @assignments[submission.assignment_id] - gradingPeriod = @indexedGradingPeriods[selectedPeriodId] - effectiveDueAt = assignment.due_at - - if assignment.has_overrides && assignment.overrides - IDsByOverrideType = { - "sectionOverrides": student.sections - "groupOverrides": student.group_ids - "studentOverrides": [student.id] - } - - getOverridesForType = ((typeIds, overrideType) => - _.map typeIds, (typeId) => - @indexedOverrides()[overrideType]?[assignment.id]?[typeId]).bind(this) - - allOverridesForSubmission = _.chain(IDsByOverrideType) - .map(getOverridesForType) - .flatten() - .compact() - .value() - - overrideDates = _.chain(allOverridesForSubmission) - .pluck('due_at') - .map((dateString) -> tz.parse(dateString)) - .value() - - if overrideDates.length > 0 - nullDueAtsExist = _.any(overrideDates, (date) -> _.isNull(date)) - effectiveDueAt = if nullDueAtsExist then null else _.max(overrideDates) - else - return true if assignment.only_visible_to_overrides - - showSubmission = @lastGradingPeriodAndDueAtNull(gradingPeriod, effectiveDueAt) || @dateIsInGradingPeriod(gradingPeriod, effectiveDueAt) - !showSubmission - - lastGradingPeriodAndDueAtNull: (gradingPeriod, dueAt) -> - gradingPeriod.is_last && _.isNull(dueAt) - - dateIsInGradingPeriod: (gradingPeriod, date) -> - return false if _.isNull(date) - startDate = tz.parse(gradingPeriod.start_date) - endDate = tz.parse(gradingPeriod.end_date) - startDate < date && date <= endDate + (SubmissionCell[assignment.grading_type] || SubmissionCell).formatter(row, col, submission, assignment, student, formatterOpts) staticCellFormatter: (row, col, val) -> "
#{htmlEscape(val)}
" - uneditableCellOutsideOfGradingPeriodFormatter: (row, col) -> - """ -
- #{htmlEscape(I18n.t("Submission in another grading period"))} -
-
- """ - - uneditableCellFormatter: (row, col) -> - "
" + lockedAndHiddenGradeCellFormatter: (row, col, tooltipKey) -> + if tooltipKey + tooltip = GRADEBOOK_TRANSLATIONS["submission_tooltip_#{tooltipKey}"] + """ +
+ #{htmlEscape(tooltip)} +
+
+ """ + else + "
" groupTotalFormatter: (row, col, val, columnDef, student) => return '' unless val? @@ -1084,7 +971,7 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> initHeader: => @drawSectionSelectButton() if @sections_enabled - @drawGradingPeriodSelectButton() if @mgpEnabled + @drawGradingPeriodSelectButton() if @gradingPeriodsEnabled $settingsMenu = $('.gradebook_dropdown') showConcludedEnrollmentsEl = $settingsMenu.find("#show_concluded_enrollments") @@ -1708,7 +1595,7 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> currentPeriodId == "0" hideAggregateColumns: -> - return false unless @mgpEnabled + return false unless @gradingPeriodsEnabled return false if @options.all_grading_periods_totals selectedPeriodId = @getGradingPeriodToShow() @isAllGradingPeriods(selectedPeriodId) diff --git a/app/coffeescripts/gradebook2/GradebookTranslations.coffee b/app/coffeescripts/gradebook2/GradebookTranslations.coffee index d7fe8b1364c..aa13fb363c7 100644 --- a/app/coffeescripts/gradebook2/GradebookTranslations.coffee +++ b/app/coffeescripts/gradebook2/GradebookTranslations.coffee @@ -14,6 +14,9 @@ define ['i18n!gradebook2'], (I18n) -> submission_tooltip_media_recording: I18n.t("Media Recording Submission") submission_tooltip_online_quiz: I18n.t("Quiz Submission") submission_tooltip_turnitin: I18n.t('Has Turnitin score') + submission_tooltip_not_in_any_grading_period: I18n.t("This submission is not in any grading period"), + submission_tooltip_in_another_grading_period: I18n.t("This submission is in another grading period"), + submission_tooltip_in_closed_grading_period: I18n.t("This submission is in a closed grading period"), submission_update_error: I18n.t('There was an error updating this assignment. Please refresh the page and try again.') submission_too_many_points_warning: I18n.t("This student was just awarded an unusually high grade.") submission_negative_points_warning: I18n.t("This student was just awarded negative points.") diff --git a/app/coffeescripts/gradebook2/SubmissionCell.coffee b/app/coffeescripts/gradebook2/SubmissionCell.coffee index 727de9f17cd..18c2ca7791d 100644 --- a/app/coffeescripts/gradebook2/SubmissionCell.coffee +++ b/app/coffeescripts/gradebook2/SubmissionCell.coffee @@ -72,7 +72,7 @@ define [ validate: () -> { valid: true, msg: null } - @formatter: (row, col, submission, assignment, student) -> + @formatter: (row, col, submission, assignment, student, opts = {}) -> if submission.excused grade = "EX" else @@ -85,7 +85,14 @@ define [ if grade && assignment?.grading_type == "percent" grade = grade.toString() + "%" - this.prototype.cellWrapper(grade, {submission: submission, assignment: assignment, editable: false, student: student}) + this.prototype.cellWrapper(grade, { + submission: submission, + assignment: assignment, + editable: false, + student: student, + isLocked: !!opts.isLocked, + tooltip: opts.tooltip + }) cellWrapper: (innerContents, options = {}) -> opts = $.extend({}, { @@ -94,14 +101,16 @@ define [ student: { isInactive: false, isConcluded: false, - } + }, + isLocked: false }, options) opts.submission ||= @opts.item[@opts.column.field] opts.assignment ||= @opts.column.object submission_type = opts.submission.submission_type if opts.submission?.submission_type || null specialClasses = SubmissionCell.classesBasedOnSubmission(opts.submission, opts.assignment) - specialClasses.push("grayed-out") if opts.student.isInactive || opts.student.isConcluded - specialClasses.push("cannot_edit") if opts.student.isConcluded + specialClasses.push("grayed-out") if opts.student.isInactive || opts.student.isConcluded || opts.isLocked + specialClasses.push("cannot_edit") if opts.student.isConcluded || opts.isLocked + specialClasses.push(opts.tooltip) if opts.tooltip opts.editable = false if opts.student.isConcluded opts.classes += ' no_grade_yet ' unless opts.submission.grade && opts.submission.workflow_state != 'pending_review' @@ -177,7 +186,7 @@ define [ @$input = @$wrapper.find('input').focus().select() class SubmissionCell.letter_grade extends SubmissionCell - @formatter: (row, col, submission, assignment, student) -> + @formatter: (row, col, submission, assignment, student, opts={}) -> innerContents = if submission.excused "EX" else if submission.score? @@ -185,16 +194,16 @@ define [ else submission.grade - SubmissionCell.prototype.cellWrapper(innerContents, {submission: submission, assignment: assignment, editable: false, student: student}) + SubmissionCell.prototype.cellWrapper(innerContents, {submission: submission, assignment: assignment, editable: false, student: student, isLocked: !!opts.isLocked, tooltip: opts.tooltip}) class SubmissionCell.gpa_scale extends SubmissionCell - @formatter: (row, col, submission, assignment, student) -> + @formatter: (row, col, submission, assignment, student, opts={}) -> innerContents = if submission.excused "EX" else submission.grade - SubmissionCell.prototype.cellWrapper(innerContents, {submission: submission, assignment: assignment, editable: false, student: student, classes: "gpa_scale_cell"}) + SubmissionCell.prototype.cellWrapper(innerContents, {submission: submission, assignment: assignment, editable: false, student: student, classes: "gpa_scale_cell", isLocked: !!opts.isLocked, tooltip: opts.tooltip}) class SubmissionCell.pass_fail extends SubmissionCell @@ -231,9 +240,9 @@ define [ aria-label="#{htmlEscape cssClass}">#{htmlEscape cssClass}#{checkboxButtonTemplate(iconClass)} """, options) - @formatter: (row, col, submission, assignment, student) -> + @formatter: (row, col, submission, assignment, student, opts={}) -> return SubmissionCell.formatter.apply(this, arguments) unless submission.grade? - pass_fail::htmlFromSubmission({ submission, assignment, editable: false}) + pass_fail::htmlFromSubmission({ submission, assignment, editable: false, isLocked: opts.isLocked, tooltip: opts.tooltip }) init: () -> @$wrapper = $(@cellWrapper()) diff --git a/app/jsx/account_course_user_search/AccountsTreeStore.jsx b/app/jsx/account_course_user_search/AccountsTreeStore.jsx index 8d0c8801bfd..0205fe9c7d9 100644 --- a/app/jsx/account_course_user_search/AccountsTreeStore.jsx +++ b/app/jsx/account_course_user_search/AccountsTreeStore.jsx @@ -1,7 +1,8 @@ define([ + "react", "./createStore", "underscore" -], function(createStore, _) { +], function(React, createStore, _) { var { string, shape, arrayOf } = React.PropTypes; diff --git a/app/jsx/gradebook/SubmissionStateMap.jsx b/app/jsx/gradebook/SubmissionStateMap.jsx new file mode 100644 index 00000000000..d214f61c588 --- /dev/null +++ b/app/jsx/gradebook/SubmissionStateMap.jsx @@ -0,0 +1,184 @@ +define([ + 'underscore', + 'timezone', + 'i18n!gradebook2' +], function(_, tz, I18n) { + + const TOOLTIP_KEYS = { + NOT_IN_ANY_GP: "not_in_any_grading_period", + IN_ANOTHER_GP: "in_another_grading_period", + IN_CLOSED_GP: "in_closed_grading_period", + NONE: null + }; + + function visibleToStudent(assignment, student) { + if (!assignment.only_visible_to_overrides) return true; + return _.contains(assignment.assignment_visibility, student.id); + } + + function assignedToStudent(assignment, overriddenDate) { + if (!assignment.only_visible_to_overrides) return true; + return overriddenDate !== undefined; + } + + function isAllGradingPeriods(periodId) { + return periodId === "0"; + } + + function lastGradingPeriodAndDueAtNull(gradingPeriod, dueAt) { + return gradingPeriod.is_last && dueAt === null; + } + + function dateIsInGradingPeriod(gradingPeriod, date) { + if (date === null) return false; + return tz.parse(gradingPeriod.start_date) < date && date <= tz.parse(gradingPeriod.end_date); + } + + function addStudentID(student, collection = []) { + return collection.concat([student.id]); + } + + function studentIDCollections(students) { + const sections = {}; + const groups = {}; + + students.forEach(function(student) { + student.sections.forEach(sectionID => sections[sectionID] = addStudentID(student, sections[sectionID])); + student.group_ids.forEach(groupID => groups[groupID] = addStudentID(student, groups[groupID])); + }); + + return { studentIDsInSections: sections, studentIDsInGroups: groups }; + } + + function studentIDsOnOverride(override, sections, groups) { + if (override.student_ids) { + return override.student_ids; + } else if (override.course_section_id && sections[override.course_section_id]) { + return sections[override.course_section_id]; + } else if (override.group_id && groups[override.group_id]) { + return groups[override.group_id]; + } else { + return []; + } + } + + function getLatestDefinedDate(newDate, existingDate) { + if (existingDate === undefined || newDate === null) { + return newDate; + } else if (existingDate !== null && newDate > existingDate) { + return newDate; + } else { + return existingDate; + } + } + + function indexOverrides(assignments, students) { + const { studentIDsInSections, studentIDsInGroups } = studentIDCollections(students); + const overrides = students.reduce(function(obj, student) { + obj[student.id] = {}; + return obj; + }, {}); + + _.each(assignments, function(assignment) { + if (!assignment.has_overrides || !assignment.overrides) return; + + assignment.overrides.forEach(function(override) { + const studentIDs = studentIDsOnOverride(override, studentIDsInSections, studentIDsInGroups); + + studentIDs.forEach(function(studentID) { + overrides[studentID] = overrides[studentID] || {}; + const existingDate = overrides[studentID][assignment.id]; + const newDate = tz.parse(override.due_at); + overrides[studentID][assignment.id] = getLatestDefinedDate(newDate, existingDate); + }); + }); + }); + + return overrides; + } + + function getGradingPeriodForDueAt(gradingPeriods, dueAt) { + return _.find(gradingPeriods, function(period) { + return lastGradingPeriodAndDueAtNull(period, dueAt) || + dateIsInGradingPeriod(period, dueAt); + }); + } + + function cellMapForSubmission(assignment, student, overriddenDate, gradingPeriodsEnabled, selectedGradingPeriodID, gradingPeriods, isAdmin) { + if (!visibleToStudent(assignment, student)) { + return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NONE }; + } else if (gradingPeriodsEnabled) { + return cellMappingsForMultipleGradingPeriods(assignment, student, overriddenDate, selectedGradingPeriodID, gradingPeriods, isAdmin); + } else { + return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE }; + } + } + + function cellMappingsForMultipleGradingPeriods(assignment, student, overriddenDate, selectedGradingPeriodID, gradingPeriods, isAdmin) { + const specificPeriodSelected = !isAllGradingPeriods(selectedGradingPeriodID); + const effectiveDueAt = overriddenDate === undefined ? assignment.due_at : overriddenDate; + const gradingPeriodForDueAt = getGradingPeriodForDueAt(gradingPeriods, effectiveDueAt); + + if (specificPeriodSelected && visibleToStudent(assignment, student) && !assignedToStudent(assignment, overriddenDate)) { + return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NOT_IN_ANY_GP }; + } else if (specificPeriodSelected && !gradingPeriodForDueAt) { + return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.NOT_IN_ANY_GP }; + } else if (specificPeriodSelected && selectedGradingPeriodID !== gradingPeriodForDueAt.id) { + return { locked: true, hideGrade: true, tooltip: TOOLTIP_KEYS.IN_ANOTHER_GP }; + } else if (!isAdmin && (gradingPeriodForDueAt || {}).closed) { + return { locked: true, hideGrade: false, tooltip: TOOLTIP_KEYS.IN_CLOSED_GP }; + } else { + return { locked: false, hideGrade: false, tooltip: TOOLTIP_KEYS.NONE }; + } + } + + class SubmissionState { + constructor({ gradingPeriodsEnabled, selectedGradingPeriodID, gradingPeriods, isAdmin }) { + this.gradingPeriodsEnabled = gradingPeriodsEnabled; + this.selectedGradingPeriodID = selectedGradingPeriodID; + this.gradingPeriods = gradingPeriods; + this.isAdmin = isAdmin; + this.overrides = {}; + this.submissionCellMap = {}; + this.submissionMap = {}; + } + + setup(students, assignments) { + const newOverrides = indexOverrides(assignments, students); + this.overrides = Object.assign(this.overrides, newOverrides); + + students.forEach((student) => { + this.submissionCellMap[student.id] = {}; + this.submissionMap[student.id] = {}; + _.each(assignments, (assignment) => { + this.setSubmissionCellState(student, assignment, student[`assignment_${assignment.id}`]); + }); + }); + } + + setSubmissionCellState(student, assignment, submission = { assignment_id: assignment.id, user_id: student.id }) { + this.submissionMap[student.id][assignment.id] = submission; + const params = [ + assignment, + student, + this.overrides[student.id][assignment.id], + this.gradingPeriodsEnabled, + this.selectedGradingPeriodID, + this.gradingPeriods, + this.isAdmin + ]; + + this.submissionCellMap[student.id][assignment.id] = cellMapForSubmission(...params); + } + + getSubmission(user_id, assignment_id) { + return this.submissionMap[user_id][assignment_id]; + } + + getSubmissionState({ user_id, assignment_id }) { + return this.submissionCellMap[user_id][assignment_id]; + } + }; + + return SubmissionState; +}) \ No newline at end of file diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 6bee5bbd387..6c74bfe9b27 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -124,7 +124,7 @@ class GradingPeriod < ActiveRecord::Base def as_json_with_user_permissions(user) as_json( - only: [:id, :title, :start_date, :end_date], + only: [:id, :title, :start_date, :end_date, :close_date], permissions: { user: user }, methods: :is_last ).fetch(:grading_period) diff --git a/spec/coffeescripts/gradebook2/GradebookSpec.coffee b/spec/coffeescripts/gradebook2/GradebookSpec.coffee index 129ea31c02d..78ac61e9528 100644 --- a/spec/coffeescripts/gradebook2/GradebookSpec.coffee +++ b/spec/coffeescripts/gradebook2/GradebookSpec.coffee @@ -43,514 +43,11 @@ define [ indexedOverrides: Gradebook.prototype.indexedOverrides indexedGradingPeriods: _.indexBy(@gradingPeriods, 'id') - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with no overrides", - setupThis: (options) -> - customOptions = options || {} - defaults = - mgpEnabled: true - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - @overrides = { studentOverrides: {}, sectionOverrides: {} } - teardown: -> - - test 'returns false if multiple grading periods is not enabled', -> - self = @setupThis(mgpEnabled: false) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - deepEqual result, false - - test 'returns false if "All Grading Periods" is selected', -> - self = @setupThis(mgpEnabled: true, isAllGradingPeriods: -> true) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - deepEqual result, false - - test 'returns false if the assignment has a null due_at and the last grading period is selected', -> - assignments = { '1': { id: '1', has_overrides: false, due_at: null } } - self = @setupThis(assignments: assignments, gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - test 'returns true if the assignment has a null due_at and the last grading period is not selected', -> - assignments = { '1': { id: '1', has_overrides: false, due_at: null } } - self = @setupThis(assignments: assignments) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, true - - test 'returns false if the assignment due_at falls in the selected grading period', -> - assignments = { '1': { id: '1', has_overrides: false, due_at: tz.parse('2015-04-15T06:00:00Z') } } - self = @setupThis(assignments: assignments, dateIsInGradingPeriod: -> true) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the assignment due_at falls outside of the selected grading period', -> - assignments = { '1': { id: '1', has_overrides: false, due_at: tz.parse('2015-05-15T06:00:00Z') } } - self = @setupThis(assignments: assignments, dateIsInGradingPeriod: -> false) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-05-15T06:00:00Z') - deepEqual result, true - - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with one student override that applies to the student", - setupThis: (options, overrides) -> - customOptions = options || {} - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-05-15T06:00:00Z'), overrides: overrides } } - defaults = - mgpEnabled: true - assignments: assignments - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - generateOverrides: (dueAt) -> - [ - { student_ids: ['5'], due_at: dueAt } - ] - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - teardown: -> - - test 'returns false if the due_at on the override falls within the grading period', -> - overrides = @generateOverrides('2015-04-15T06:00:00Z') - self = @setupThis({ dateIsInGradingPeriod: -> true }, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the due_at on the override falls outside of the grading period', -> - overrides = @generateOverrides('2015-06-15T06:00:00Z') - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-06-15T06:00:00Z') - deepEqual result, true - - test 'returns true if the due_at on the override is null and the grading period is not the last', -> - overrides = @generateOverrides(null) - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok _.isNull(dateIsInGradingPeriodSpy.args[0][1]) - deepEqual result, true - - test 'returns false if the due_at on the override is null and the grading period is the last', -> - overrides = @generateOverrides(null) - self = @setupThis({ gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true }, overrides) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with one section override that applies to the student", - setupThis: (options, overrides) -> - customOptions = options || {} - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-05-15T06:00:00Z'), overrides: overrides } } - defaults = - mgpEnabled: true - assignments: assignments - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - generateOverrides: (dueAt) -> - [ - { student_ids: ['5'], due_at: dueAt } - ] - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - teardown: -> - - test 'returns false if the due_at on the override falls within the grading period', -> - overrides = @generateOverrides('2015-04-15T06:00:00Z') - self = @setupThis({ dateIsInGradingPeriod: -> true }, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the due_at on the override falls outside of the grading period', -> - overrides = @generateOverrides('2015-06-15T06:00:00Z') - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-06-15T06:00:00Z') - deepEqual result, true - - test 'returns true if the due_at on the override is null and the grading period is not the last', -> - overrides = @generateOverrides(null) - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok _.isNull(dateIsInGradingPeriodSpy.args[0][1]) - deepEqual result, true - - test 'returns false if the due_at on the override is null and the grading period is the last', -> - overrides = @generateOverrides(null) - self = @setupThis({ gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true }, overrides) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with one group that applies to the student", - setupThis: (options, overrides) -> - customOptions = options || {} - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-05-15T06:00:00Z'), overrides: overrides } } - defaults = - mgpEnabled: true - assignments: assignments - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - generateOverrides: (dueAt) -> - [ - { group_id: '202', due_at: dueAt } - ] - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'], group_ids: ['202'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - teardown: -> - - test 'returns false if the due_at on the override falls within the grading period', -> - overrides = @generateOverrides('2015-04-15T06:00:00Z') - self = @setupThis({ dateIsInGradingPeriod: -> true }, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the due_at on the override falls outside of the grading period', -> - overrides = @generateOverrides('2015-06-15T06:00:00Z') - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-06-15T06:00:00Z') - deepEqual result, true - - test 'returns true if the due_at on the override is null and the grading period is not the last', -> - overrides = @generateOverrides(null) - self = @setupThis({}, overrides) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok _.isNull(dateIsInGradingPeriodSpy.args[0][1]) - deepEqual result, true - - test 'returns false if the due_at on the override is null and the grading period is the last', -> - overrides = @generateOverrides(null) - self = @setupThis({ gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true }, overrides) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with one override that does not apply to the student", - setupThis: (options) -> - customOptions = options || {} - defaults = - mgpEnabled: true - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - @overrides = { - studentOverrides: { '1': { '18': { student_ids: ['18'], due_at: '2015-04-15T06:00:00Z' } } } - sectionOverrides: {} - } - teardown: -> - - test 'returns true if the assignment is only visible to overrides', -> - assignments = { '1': { id: '1', has_overrides: true, due_at: null, only_visible_to_overrides: true } } - self = @setupThis( assignments: assignments) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - deepEqual result, true - - test 'returns true if the assignment due_at is outside of the grading period', -> - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-05-15T06:00:00Z') } } - self = @setupThis(assignments: assignments) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-05-15T06:00:00Z') - deepEqual result, true - - test 'returns false if the assignment due_at is within the grading period', -> - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-04-15T06:00:00Z') } } - self = @setupThis(assignments: assignments, dateIsInGradingPeriod: -> true) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the assignment due_at is null and the grading period is not the last', -> - assignments = { '1': { id: '1', has_overrides: true, due_at: null } } - self = @setupThis(assignments: assignments) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok dateIsInGradingPeriodSpy.called - ok _.isNull(dateIsInGradingPeriodSpy.args[0][1]) - deepEqual result, true - - test 'returns false if the assignment due_at is null and the grading period is the last', -> - assignments = { '1': { id: '1', has_overrides: true, due_at: null } } - self = @setupThis(assignments: assignments, gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - result = isOutsidePeriod(@submission, @student, @gradingPeriods, @overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - module "Gradebook2#submissionOutsideOfGradingPeriod - assignment with two overrides that apply to the student", - setupThis: (options, overrides) -> - customOptions = options || {} - assignments = { '1': { id: '1', has_overrides: true, due_at: tz.parse('2015-05-15T06:00:00Z'), overrides: overrides } } - defaults = - mgpEnabled: true - assignments: assignments - isAllGradingPeriods: -> false - gradingPeriodToShow: '8' - lastGradingPeriodAndDueAtNull: -> false - dateIsInGradingPeriod: -> false - - _.defaults customOptions, defaults, gradebookStubs() - - generateOverrides: (date1, date2) -> - [ - { student_ids: ['5'], due_at: date1 } - { course_section_id: '101', assignment_id: '1', due_at: date2 } - ] - - setup: -> - @subOutsideOfPeriod = Gradebook.prototype.submissionOutsideOfGradingPeriod - @submission = { assignment_id: '1' } - @student = { id: '5', sections: ['101','102','103'] } - @gradingPeriods = { - '8': { id: '8', start_date: '2015-04-01T06:00:00Z', end_date: '2015-05-01T05:59:59Z', is_last: false } - '10': { id: '10', start_date: '2015-05-05T06:00:00Z', end_date: '2015-06-01T05:59:59Z', is_last: true } - } - teardown: -> - - test 'returns false if the latest date of the two overrides falls within the grading period', -> - overrides = @generateOverrides('2015-03-01T06:00:00Z', '2015-04-15T06:00:00Z') - self = @setupThis({ dateIsInGradingPeriod: -> true }, overrides) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-04-15T06:00:00Z') - deepEqual result, false - - test 'returns true if the latest date of the two overrides falls outside the grading period' + - '(even if the earlier date falls within the grading period)', -> - overrides = @generateOverrides('2015-04-15T06:00:00Z', '2015-05-15T06:00:00Z') - self = @setupThis({}, overrides) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok +dateIsInGradingPeriodSpy.args[0][1] == +tz.parse('2015-05-15T06:00:00Z') - deepEqual result, true - - test 'returns false if either date is null and the last grading period is selected', -> - overrides = @generateOverrides(null, '2015-05-15T06:00:00Z') - self = @setupThis({ gradingPeriodToShow: '10', lastGradingPeriodAndDueAtNull: -> true }, overrides) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - lastGradingPeriodAndDueAtNullSpy = @spy(self, 'lastGradingPeriodAndDueAtNull') - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok lastGradingPeriodAndDueAtNullSpy.called - ok _.isNull(lastGradingPeriodAndDueAtNullSpy.args[0][1]) - deepEqual result, false - - test 'returns true if either date is null and the last grading period is not selected', -> - overrides = @generateOverrides(null, '2015-05-15T06:00:00Z') - self = @setupThis({}, overrides) - isOutsidePeriod = @subOutsideOfPeriod.bind(self) - dateIsInGradingPeriodSpy = @spy(self, 'dateIsInGradingPeriod') - result = isOutsidePeriod(@submission, @student, @gradingPeriods, overrides) - - ok dateIsInGradingPeriodSpy.called - ok _.isNull(dateIsInGradingPeriodSpy.args[0][1]) - deepEqual result, true - - module "Gradebook2#lastGradingPeriodAndDueAtNull", - setup: -> - @lastGradingPeriodAndDueAtNull = Gradebook.prototype.lastGradingPeriodAndDueAtNull - teardown: -> - - test 'returns true if it is the last grading period and the due at is null', -> - gradingPeriod = { is_last: true } - dueAt = null - ok @lastGradingPeriodAndDueAtNull(gradingPeriod, dueAt) - - test 'returns false if it is not the last grading period', -> - gradingPeriod = { is_last: false } - dueAt = null - notOk @lastGradingPeriodAndDueAtNull(gradingPeriod, dueAt) - - test 'returns false if the dueAt is something other than null', -> - gradingPeriod = { is_last: true } - dueAt = tz.parse('2015-05-15T06:00:00Z') - notOk @lastGradingPeriodAndDueAtNull(gradingPeriod, dueAt) - - module "Gradebook2#dateIsInGradingPeriod", - setup: -> - @dateIsInGradingPeriod = Gradebook.prototype.dateIsInGradingPeriod - @gradingPeriod = { start_date: tz.parse('2015-04-01T06:00:00Z'), end_date: tz.parse('2015-05-01T06:00:00Z') } - teardown: -> - - test 'returns false if the date is null', -> - date = null - notOk @dateIsInGradingPeriod(@gradingPeriod, date) - - test 'returns true if the date falls between the grading period start date and end date', -> - date = tz.parse('2015-04-15T06:00:00Z') - ok @dateIsInGradingPeriod(@gradingPeriod, date) - - test 'returns false if the date is before the grading period start date', -> - date = tz.parse('2015-03-15T06:00:00Z') - notOk @dateIsInGradingPeriod(@gradingPeriod, date) - - test 'returns false if the date is after the grading period end date', -> - date = tz.parse('2015-05-15T06:00:00Z') - notOk @dateIsInGradingPeriod(@gradingPeriod, date) - - test 'returns false if the date is the same as the grading period start date', -> - date = tz.parse('2015-04-01T06:00:00Z') - notOk @dateIsInGradingPeriod(@gradingPeriod, date) - - test 'returns true if the date is the same as the grading period end date', -> - date = tz.parse('2015-05-01T06:00:00Z') - ok @dateIsInGradingPeriod(@gradingPeriod, date) - module "Gradebook2#hideAggregateColumns", setupThis: (options) -> customOptions = options || {} defaults = - mgpEnabled: true + gradingPeriodsEnabled: true getGradingPeriodToShow: -> '1' options: all_grading_periods_totals: false @@ -562,12 +59,12 @@ define [ teardown: -> test 'returns false if multiple grading periods is disabled', -> - self = @setupThis(mgpEnabled: false, isAllGradingPeriods: -> false) + self = @setupThis(gradingPeriodsEnabled: false, isAllGradingPeriods: -> false) notOk @hideAggregateColumns.call(self) test 'returns false if multiple grading periods is disabled, even if isAllGradingPeriods is true', -> self = @setupThis - mgpEnabled: false + gradingPeriodsEnabled: false getGradingPeriodToShow: -> '0' isAllGradingPeriods: -> true diff --git a/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee b/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee index 9fbd4970ff9..392faf6aaaa 100644 --- a/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee +++ b/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee @@ -59,19 +59,39 @@ define [ submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 'happy'}, { }, student) notEqual submissionCellResponse.indexOf("grayed-out"), -1 + test "#class.formatter, isLocked: true adds grayed-out", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("grayed-out") > -1 + + test "#class.formatter, isLocked: true adds cannot_edit", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("cannot_edit") > -1 + + test "#class.formatter, isLocked: false doesn't add grayed-out", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("grayed-out"), -1 + + test "#class.formatter, isLocked: false doesn't add cannot_edit", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("cannot_edit"), -1 + + test "#class.formatter, tooltip adds your text to the special classes", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { tooltip: "dora_the_explorer" }) + ok submissionCellResponse.indexOf("dora_the_explorer") > -1 + test "#class.formatter, isInactive: false doesn't add grayed-out", -> student = { isInactive: false } - submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10}, { }, student) + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10 }, { }, student) equal submissionCellResponse.indexOf("grayed-out"), -1 test "#class.formatter, isConcluded adds grayed-out", -> student = { isConcluded: true } - submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10}, { }, student) + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10 }, { }, student) notEqual submissionCellResponse.indexOf("grayed-out"), -1 test "#class.formatter, isConcluded doesn't have grayed-out", -> student = { isConcluded: false } - submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10}, { }, student) + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 10 }, { }, student) equal submissionCellResponse.indexOf("grayed-out"), -1 test "#letter_grade.formatter, shows EX when submission is excused", -> @@ -88,3 +108,63 @@ define [ @stub(SubmissionCell.prototype, 'cellWrapper').withArgs('B').returns('ok') formattedResponse = SubmissionCell.letter_grade.formatter(0, 0, {grade: 'B'}, {}, {}) equal formattedResponse, 'ok' + + test "#letter_grade.formatter, isLocked: true adds grayed-out", -> + submissionCellResponse = SubmissionCell.letter_grade.formatter(0, 0, { grade: "A" }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("grayed-out") > -1 + + test "#letter_grade.formatter, isLocked: true adds cannot_edit", -> + submissionCellResponse = SubmissionCell.letter_grade.formatter(0, 0, { grade: "A" }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("cannot_edit") > -1 + + test "#letter_grade.formatter, isLocked: false doesn't add grayed-out", -> + submissionCellResponse = SubmissionCell.letter_grade.formatter(0, 0, { grade: "A" }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("grayed-out"), -1 + + test "#letter_grade.formatter, isLocked: false doesn't add cannot_edit", -> + submissionCellResponse = SubmissionCell.letter_grade.formatter(0, 0, { grade: "A" }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("cannot_edit"), -1 + + test "#letter_grade.formatter, tooltip adds your text to the special classes", -> + submissionCellResponse = SubmissionCell.letter_grade.formatter(0, 0, { grade: "A" }, {}, {}, { tooltip: "dora_the_explorer" }) + ok submissionCellResponse.indexOf("dora_the_explorer") > -1 + + test "#gpa_scale.formatter, isLocked: true adds grayed-out", -> + submissionCellResponse = SubmissionCell.gpa_scale.formatter(0, 0, { grade: 3.2 }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("grayed-out") > -1 + + test "#gpa_scale.formatter, isLocked: true adds cannot_edit", -> + submissionCellResponse = SubmissionCell.gpa_scale.formatter(0, 0, { grade: 3.2 }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("cannot_edit") > -1 + + test "#gpa_scale.formatter, isLocked: false doesn't add grayed-out", -> + submissionCellResponse = SubmissionCell.gpa_scale.formatter(0, 0, { grade: 3.2 }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("grayed-out"), -1 + + test "#gpa_scale.formatter, isLocked: false doesn't add cannot_edit", -> + submissionCellResponse = SubmissionCell.gpa_scale.formatter(0, 0, { grade: 3.2 }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("cannot_edit"), -1 + + test "#gpa_scale.formatter, tooltip adds your text to the special classes", -> + submissionCellResponse = SubmissionCell.gpa_scale.formatter(0, 0, { grade: 3.2 }, {}, {}, { tooltip: "dora_the_explorer" }) + ok submissionCellResponse.indexOf("dora_the_explorer") > -1 + + test "#pass_fail.formatter, isLocked: true adds grayed-out", -> + submissionCellResponse = SubmissionCell.pass_fail.formatter(0, 0, { grade: "complete" }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("grayed-out") > -1 + + test "#pass_fail.formatter, isLocked: true adds cannot_edit", -> + submissionCellResponse = SubmissionCell.pass_fail.formatter(0, 0, { grade: "complete" }, {}, {}, { isLocked: true }) + ok submissionCellResponse.indexOf("cannot_edit") > -1 + + test "#pass_fail.formatter, isLocked: false doesn't add grayed-out", -> + submissionCellResponse = SubmissionCell.pass_fail.formatter(0, 0, { grade: "complete" }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("grayed-out"), -1 + + test "#pass_fail.formatter, isLocked: false doesn't add cannot_edit", -> + submissionCellResponse = SubmissionCell.pass_fail.formatter(0, 0, { grade: "complete" }, {}, {}, { isLocked: false }) + equal submissionCellResponse.indexOf("cannot_edit"), -1 + + test "#pass_fail.formatter, tooltip adds your text to the special classes", -> + submissionCellResponse = SubmissionCell.pass_fail.formatter(0, 0, { grade: "complete" }, {}, {}, { tooltip: "dora_the_explorer" }) + ok submissionCellResponse.indexOf("dora_the_explorer") > -1 diff --git a/spec/javascripts/jsx/gradebook/SubmissionStateMapGradeVisibilitySpec.jsx b/spec/javascripts/jsx/gradebook/SubmissionStateMapGradeVisibilitySpec.jsx new file mode 100644 index 00000000000..e73d62c9020 --- /dev/null +++ b/spec/javascripts/jsx/gradebook/SubmissionStateMapGradeVisibilitySpec.jsx @@ -0,0 +1,679 @@ +define([ + 'underscore', + 'timezone', + 'jsx/gradebook/SubmissionStateMap' +], (_, tz, SubmissionStateMap) => { + const student = { + id: '1', + group_ids: ['1'], + sections: ['1'] + }; + + const tooltipKeys = { + NOT_IN_ANY_GP: "not_in_any_grading_period", + IN_ANOTHER_GP: "in_another_grading_period", + IN_CLOSED_GP: "in_closed_grading_period", + NONE: null + }; + + function createMap(opts={}) { + const defaults = { + gradingPeriodsEnabled: false, + selectedGradingPeriodID: '0', + isAdmin: false, + gradingPeriods: [] + }; + + const params = Object.assign(defaults, opts); + return new SubmissionStateMap(params); + } + + function createAndSetupMap(assignment, opts={}) { + const map = createMap(opts); + const assignments = {}; + assignments[assignment.id] = assignment; + map.setup([student], assignments); + return map; + } + + function createGradingPeriod(opts={}) { + const defaults = { + id: '1', + is_last: false, + closed: false + }; + + return Object.assign(defaults, opts); + } + + function createOverride({ type, id, dueAt }={}) { + const override = { + assignment_id: '1', + due_at: dueAt, + }; + + if (type === 'student') { + override.student_ids = [id]; + } else if (type === 'section') { + override.course_section_id = id; + } else { + override.group_id = id; + } + + return override; + } + + function createAssignment({ dueAt, overrides, gradedButNotAssigned }={}) { + const assignment = { + id: '1', + only_visible_to_overrides: false, + assignment_visibility: [], + due_at: null, + has_overrides: false + }; + + if (dueAt === undefined) { + assignment.only_visible_to_overrides = true; + } else { + assignment.due_at = tz.parse(dueAt); + } + + if (overrides) { + assignment.has_overrides = true; + assignment.overrides = overrides; + + const overrideForStudent = _.any(overrides, function(override) { + const includesStudent = override.student_ids && _.contains(override.student_ids, student.id); + const includesSection = _.contains(student.sections, override.course_section_id); + const includesGroup = _.contains(student.group_ids, override.group_id); + return includesStudent || includesSection || includesGroup; + }); + + const studentGradedButNotAssigned = gradedButNotAssigned && _.contains(gradedButNotAssigned, student.id); + + if (overrideForStudent || studentGradedButNotAssigned) assignment.assignment_visibility.push(student.id); + } + + return assignment; + } + + // TODO: the spec setup above should live in a spec helper -- at the + // time this is being written a significant amount of work is needed + // to be able to require javascript files that live in the spec directory + + const OTHER_STUDENT_ID = '2'; + + module('SubmissionStateMap with MGP disabled'); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + module('SubmissionStateMap with MGP enabled and all grading periods selected', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: '0', gradingPeriods }; + } + }); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment due in a closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment due in a non-closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_OPEN_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment due outside of any grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in a closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in a non-closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment due outside of any grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date due outside of any grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: null }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + module('SubmissionStateMap with MGP enabled and a non-closed grading period selected that is not the last grading period', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: openPeriod.id, gradingPeriods }; + } + }); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: null }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + module('SubmissionStateMap with MGP enabled and a closed grading period selected that is not the last grading period', { + setup() { + const firstClosedPeriod = createGradingPeriod({ id: '1', start_date: '2015-06-01', end_date: '2015-06-30', close_date: '2015-07-02', closed: true }); + const secondClosedPeriod = createGradingPeriod({ id: '2', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '3', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02'}); + const lastPeriod = createGradingPeriod({ id: '4', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-05-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [firstClosedPeriod, secondClosedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: secondClosedPeriod.id, gradingPeriods }; + } + }); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is not closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriod.id, gradingPeriods }; + } + }); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-12-25' }); + const lastPeriodAndClosed = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true, closed: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriodAndClosed]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriodAndClosed.id, gradingPeriods }; + } + }); + + test('submission has grade hidden for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade hidden for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade hidden for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, true); + }); + + test('submission has grade visible for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); + + test('submission has grade visible for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.hideGrade, false); + }); +}); diff --git a/spec/javascripts/jsx/gradebook/SubmissionStateMapLockingSpec.jsx b/spec/javascripts/jsx/gradebook/SubmissionStateMapLockingSpec.jsx new file mode 100644 index 00000000000..f717beb1ec4 --- /dev/null +++ b/spec/javascripts/jsx/gradebook/SubmissionStateMapLockingSpec.jsx @@ -0,0 +1,919 @@ +define([ + 'underscore', + 'timezone', + 'jsx/gradebook/SubmissionStateMap' +], (_, tz, SubmissionStateMap) => { + const student = { + id: '1', + group_ids: ['1'], + sections: ['1'] + }; + + const tooltipKeys = { + NOT_IN_ANY_GP: "not_in_any_grading_period", + IN_ANOTHER_GP: "in_another_grading_period", + IN_CLOSED_GP: "in_closed_grading_period", + NONE: null + }; + + function createMap(opts={}) { + const defaults = { + gradingPeriodsEnabled: false, + selectedGradingPeriodID: '0', + isAdmin: false, + gradingPeriods: [] + }; + + const params = Object.assign(defaults, opts); + return new SubmissionStateMap(params); + } + + function createAndSetupMap(assignment, opts={}) { + const map = createMap(opts); + const assignments = {}; + assignments[assignment.id] = assignment; + map.setup([student], assignments); + return map; + } + + function createGradingPeriod(opts={}) { + const defaults = { + id: '1', + is_last: false, + closed: false + }; + + return Object.assign(defaults, opts); + } + + function createOverride({ type, id, dueAt }={}) { + const override = { + assignment_id: '1', + due_at: dueAt, + }; + + if (type === 'student') { + override.student_ids = [id]; + } else if (type === 'section') { + override.course_section_id = id; + } else { + override.group_id = id; + } + + return override; + } + + function createAssignment({ dueAt, overrides, gradedButNotAssigned }={}) { + const assignment = { + id: '1', + only_visible_to_overrides: false, + assignment_visibility: [], + due_at: null, + has_overrides: false + }; + + if (dueAt === undefined) { + assignment.only_visible_to_overrides = true; + } else { + assignment.due_at = tz.parse(dueAt); + } + + if (overrides) { + assignment.has_overrides = true; + assignment.overrides = overrides; + + const overrideForStudent = _.any(overrides, function(override) { + const includesStudent = override.student_ids && _.contains(override.student_ids, student.id); + const includesSection = _.contains(student.sections, override.course_section_id); + const includesGroup = _.contains(student.group_ids, override.group_id); + return includesStudent || includesSection || includesGroup; + }); + + const studentGradedButNotAssigned = gradedButNotAssigned && _.contains(gradedButNotAssigned, student.id); + + if (overrideForStudent || studentGradedButNotAssigned) assignment.assignment_visibility.push(student.id); + } + + return assignment; + } + + // TODO: the spec setup above should live in a spec helper -- at the + // time this is being written a significant amount of work is needed + // to be able to require javascript files that live in the spec directory + + const OTHER_STUDENT_ID = '2'; + + module('SubmissionStateMap with MGP disabled'); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + module('SubmissionStateMap with MGP enabled and all grading periods selected', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: '0', gradingPeriods }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with assignment due in a closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('user is admin: submission is unlocked for an assigned student with assignment due in a closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with assignment due in a non-closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_OPEN_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with assignment due outside of any grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment due in a closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('user is admin: submission is unlocked for an assigned student with overridden assignment due in a closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }) + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment due in a non-closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment due outside of any grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('user is admin: submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date due outside of any grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: null }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + module('SubmissionStateMap with MGP enabled and a non-closed grading period selected that is not the last grading period', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: openPeriod.id, gradingPeriods }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: null }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + module('SubmissionStateMap with MGP enabled and a closed grading period selected that is not the last grading period', { + setup() { + const firstClosedPeriod = createGradingPeriod({ id: '1', start_date: '2015-06-01', end_date: '2015-06-30', close_date: '2015-07-02', closed: true }); + const secondClosedPeriod = createGradingPeriod({ id: '2', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '3', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02'}); + const lastPeriod = createGradingPeriod({ id: '4', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-05-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [firstClosedPeriod, secondClosedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: secondClosedPeriod.id, gradingPeriods }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + module('SubmissionStateMap -- user is admin with MGP enabled and a closed grading period selected that is not the last grading period', { + setup() { + const firstClosedPeriod = createGradingPeriod({ id: '1', start_date: '2015-06-01', end_date: '2015-06-30', close_date: '2015-07-02', closed: true }); + const secondClosedPeriod = createGradingPeriod({ id: '2', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '3', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02'}); + const lastPeriod = createGradingPeriod({ id: '4', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-05-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [firstClosedPeriod, secondClosedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: secondClosedPeriod.id, gradingPeriods, isAdmin: true }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is not closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriod.id, gradingPeriods }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-12-25' }); + const lastPeriodAndClosed = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true, closed: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriodAndClosed]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriodAndClosed.id, gradingPeriods }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + module('SubmissionStateMap -- user is admin with MGP enabled and the last grading period selected which is closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-12-25' }); + const lastPeriodAndClosed = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true, closed: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriodAndClosed]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriodAndClosed.id, gradingPeriods, isAdmin: true }; + } + }); + + test('submission is locked for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is locked for an assigned student with assignment due outside of the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment due outside of the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is locked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date outside of the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, true); + }); + + test('submission is unlocked for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); + + test('submission is unlocked for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.locked, false); + }); +}); diff --git a/spec/javascripts/jsx/gradebook/SubmissionStateMapTooltipSpec.jsx b/spec/javascripts/jsx/gradebook/SubmissionStateMapTooltipSpec.jsx new file mode 100644 index 00000000000..1a68055d745 --- /dev/null +++ b/spec/javascripts/jsx/gradebook/SubmissionStateMapTooltipSpec.jsx @@ -0,0 +1,1069 @@ +define([ + 'underscore', + 'timezone', + 'jsx/gradebook/SubmissionStateMap' +], (_, tz, SubmissionStateMap) => { + const student = { + id: '1', + group_ids: ['1'], + sections: ['1'] + }; + + const tooltipKeys = { + NOT_IN_ANY_GP: "not_in_any_grading_period", + IN_ANOTHER_GP: "in_another_grading_period", + IN_CLOSED_GP: "in_closed_grading_period", + NONE: null + }; + + function createMap(opts={}) { + const defaults = { + gradingPeriodsEnabled: false, + selectedGradingPeriodID: '0', + isAdmin: false, + gradingPeriods: [] + }; + + const params = Object.assign(defaults, opts); + return new SubmissionStateMap(params); + } + + function createAndSetupMap(assignment, opts={}) { + const map = createMap(opts); + const assignments = {}; + assignments[assignment.id] = assignment; + map.setup([student], assignments); + return map; + } + + function createGradingPeriod(opts={}) { + const defaults = { + id: '1', + is_last: false, + closed: false + }; + + return Object.assign(defaults, opts); + } + + function createOverride({ type, id, dueAt }={}) { + const override = { + assignment_id: '1', + due_at: dueAt, + }; + + if (type === 'student') { + override.student_ids = [id]; + } else if (type === 'section') { + override.course_section_id = id; + } else { + override.group_id = id; + } + + return override; + } + + function createAssignment({ dueAt, overrides, gradedButNotAssigned }={}) { + const assignment = { + id: '1', + only_visible_to_overrides: false, + assignment_visibility: [], + due_at: null, + has_overrides: false + }; + + if (dueAt === undefined) { + assignment.only_visible_to_overrides = true; + } else { + assignment.due_at = tz.parse(dueAt); + } + + if (overrides) { + assignment.has_overrides = true; + assignment.overrides = overrides; + + const overrideForStudent = _.any(overrides, function(override) { + const includesStudent = override.student_ids && _.contains(override.student_ids, student.id); + const includesSection = _.contains(student.sections, override.course_section_id); + const includesGroup = _.contains(student.group_ids, override.group_id); + return includesStudent || includesSection || includesGroup; + }); + + const studentGradedButNotAssigned = gradedButNotAssigned && _.contains(gradedButNotAssigned, student.id); + + if (overrideForStudent || studentGradedButNotAssigned) assignment.assignment_visibility.push(student.id); + } + + return assignment; + } + + // TODO: the spec setup above should live in a spec helper -- at the + // time this is being written a significant amount of work is needed + // to be able to require javascript files that live in the spec directory + + const OTHER_STUDENT_ID = '2'; + + module('SubmissionStateMap with MGP disabled'); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: null }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, { gradingPeriodsEnabled: false }); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + module('SubmissionStateMap with MGP enabled and all grading periods selected', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: '0', gradingPeriods }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in closed period" tooltip for an assigned student with assignment due in a closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('user is admin: submission has no tooltip for an assigned student with assignment due in a closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with assignment due in a non-closed grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_OPEN_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with assignment due outside of any grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in closed period" toolip for an assigned student with overridden assignment due in a closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('user is admin: submission has no tooltip for an assigned student with overridden assignment due in a closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }) + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due in a non-closed grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due outside of any grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('user is admin: submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions, { isAdmin: true }); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-closed grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date due outside of any grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: null }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const mapOptions = Object.assign(this.mapOptions); + const map = createAndSetupMap(assignment, mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + module('SubmissionStateMap with MGP enabled and a non-closed grading period selected that is not the last grading period', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: openPeriod.id, gradingPeriods }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: null }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + module('SubmissionStateMap with MGP enabled and a closed grading period selected that is not the last grading period', { + setup() { + const firstClosedPeriod = createGradingPeriod({ id: '1', start_date: '2015-06-01', end_date: '2015-06-30', close_date: '2015-07-02', closed: true }); + const secondClosedPeriod = createGradingPeriod({ id: '2', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '3', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02'}); + const lastPeriod = createGradingPeriod({ id: '4', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-05-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [firstClosedPeriod, secondClosedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: secondClosedPeriod.id, gradingPeriods }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_OPEN_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + module('SubmissionStateMap -- user is admin with MGP enabled and a closed grading period selected that is not the last grading period', { + setup() { + const firstClosedPeriod = createGradingPeriod({ id: '1', start_date: '2015-06-01', end_date: '2015-06-30', close_date: '2015-07-02', closed: true }); + const secondClosedPeriod = createGradingPeriod({ id: '2', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '3', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02'}); + const lastPeriod = createGradingPeriod({ id: '4', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-05-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [firstClosedPeriod, secondClosedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: secondClosedPeriod.id, gradingPeriods, isAdmin: true }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_OPEN_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD}); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is not closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-09-02' }); + const lastPeriod = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriod]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriod.id, gradingPeriods }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + module('SubmissionStateMap with MGP enabled and the last grading period selected which is closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-12-25' }); + const lastPeriodAndClosed = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true, closed: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriodAndClosed]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriodAndClosed.id, gradingPeriods }; + } + }); + + test('submission has no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + test('submission shows "in closed period" tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_CLOSED_GP); + }); + + module('SubmissionStateMap -- user is admin with MGP enabled and the last grading period selected which is closed', { + setup() { + const closedPeriod = createGradingPeriod({ id: '1', start_date: '2015-07-01', end_date: '2015-07-31', close_date: '2015-08-02', closed: true }); + const openPeriod = createGradingPeriod({ id: '2', start_date: '2015-08-01', end_date: '2015-08-31', close_date: '2015-12-25' }); + const lastPeriodAndClosed = createGradingPeriod({ id: '3', start_date: '2015-09-01', end_date: '2015-09-30', close_date: '2015-10-02', is_last: true, closed: true }); + this.DATE_BEFORE_FIRST_PERIOD = '2015-06-15'; + this.DATE_IN_CLOSED_PERIOD = '2015-07-15'; + this.DATE_IN_OPEN_PERIOD = '2015-08-15'; + this.DATE_IN_SELECTED_PERIOD = '2015-09-15'; + this.DATE_AFTER_LAST_PERIOD = '2015-10-15'; + const gradingPeriods = [closedPeriod, openPeriod, lastPeriodAndClosed]; + this.mapOptions = { gradingPeriodsEnabled: true, selectedGradingPeriodID: lastPeriodAndClosed.id, gradingPeriods, isAdmin: true }; + } + }); + + test('submission shows no tooltip for an unassigned student with no submission or ungraded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "not in any period" tooltip for an unassigned student with a graded submission', function() { + const override = createOverride({ type: 'student', id: OTHER_STUDENT_ID, dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [override], gradedButNotAssigned: [student.id] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission shows "in another period" tooltip for an assigned student with assignment due in a non-selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_CLOSED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with assignment due in no grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_AFTER_LAST_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with assignment due in the selected grading period', function() { + const assignment = createAssignment({ dueAt: this.DATE_IN_SELECTED_PERIOD }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with assignment with no due date', function() { + const assignment = createAssignment({ dueAt: null }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment due in a non-selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment due in no grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment due in the selected grading period', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with no due date', function() { + const override = createOverride({ type: 'student', id: student.id, dueAt: null }); + const assignment = createAssignment({ overrides: [override] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission shows "in another period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in a non-selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.IN_ANOTHER_GP); + }); + + test('submission shows "not in any period" tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in no grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_AFTER_LAST_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_OPEN_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NOT_IN_ANY_GP); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with multiple applicable overrides with the latest due date in the selected grading period', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: this.DATE_IN_CLOSED_PERIOD }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_BEFORE_FIRST_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_IN_SELECTED_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); + + test('submission has no tooltip for an assigned student with overridden assignment with with multiple applicable overrides with at least one override with no due date', function() { + const studentOverride = createOverride({ type: 'student', id: student.id, dueAt: null }); + const sectionOverride = createOverride({ type: 'section', id: student.sections[0], dueAt: this.DATE_IN_CLOSED_PERIOD }); + const groupOverride = createOverride({ type: 'group', id: student.group_ids[0], dueAt: this.DATE_AFTER_LAST_PERIOD }); + const assignment = createAssignment({ overrides: [studentOverride, sectionOverride, groupOverride] }); + const map = createAndSetupMap(assignment, this.mapOptions); + const state = map.getSubmissionState({ user_id: student.id, assignment_id: assignment.id }); + equal(state.tooltip, tooltipKeys.NONE); + }); +}); diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index 822e2956318..a2417d63f09 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -59,6 +59,13 @@ describe GradingPeriod do expect(grading_period).to_not be_valid end + describe "#as_json_with_user_permissions" do + it "includes the close_date in the returned object" do + json = grading_period.as_json_with_user_permissions(User.new) + expect(json).to have_key("close_date") + end + end + describe "close_date" do context "grading period group belonging to an account" do it "allows setting a close_date that is different from the end_date" do @@ -200,7 +207,7 @@ describe GradingPeriod do it "does not include grading periods from the course enrollment term group if inherit is false" do group = group_helper.create_for_account(@root_account) term.update_attribute(:grading_period_group_id, group) - period_1 = period_helper.create_with_weeks_for_group(group, 5, 3) + period_helper.create_with_weeks_for_group(group, 5, 3) period_2 = period_helper.create_with_weeks_for_group(group, 3, 1) period_2.workflow_state = :deleted period_2.save @@ -234,8 +241,8 @@ describe GradingPeriod do it "does not return grading periods on the course directly" do group = group_helper.legacy_create_for_course(@course) - period_1 = period_helper.create_with_weeks_for_group(group, 5, 3) - period_2 = period_helper.create_with_weeks_for_group(group, 3, 1) + period_helper.create_with_weeks_for_group(group, 5, 3) + period_helper.create_with_weeks_for_group(group, 3, 1) expect(GradingPeriod.for(@root_account)).to match_array([]) end From bd6be749eef17076c55628955ec9b459a8a3b01e Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Tue, 9 Aug 2016 14:53:23 -0500 Subject: [PATCH 04/10] submission grade permissions respect close date tweak grade permissions on the submission model to respect close date. only root account admins have permission to grade submissions that fall in closed grading periods. closes CNVS-26716 test plan: 1. create a course with Multiple Grading Periods enabled and with at least one closed period and one open period 2. create three assignments -- one due in the closed period, one due in the open period, and one due outside of any grading period. log in as a student and submit to each assignment. 3. enter a rails console 4. verify the submission for the assignment in the closed grading period: a) is gradeable by a root-account-admin $ ... get your submission and root account admin ... $ submission.grants_right?(admin, :grade) => true b) is not gradeable by a non-root-account-admin $ ... get your submission and non-root-account-admin ... $ submission.grants_right?(teacher, :grade) => false $ submission.grants_right?(student, :grade) => false 5. verify the submission for the assignment in the open grading period: a) is gradeable by a root-account-admin $ ... get your submission and root account admin ... $ submission.grants_right?(admin, :grade) => true b) is gradeable by a non-root-account-admin teacher $ ... get your submission and teacher ... $ submission.grants_right?(teacher, :grade) => true c) is not gradeable by a non-root-account-admin student $ ... get your submission and student ... $ submission.grants_right?(student, :grade) => false 6. verify the submission for the assignment outside of any grading period: a) is gradeable by a root-account-admin $ ... get your submission and root account admin ... $ submission.grants_right?(admin, :grade) => true b) is gradeable by a non-root-account-admin teacher $ ... get your submission and teacher ... $ submission.grants_right?(teacher, :grade) => true c) is not gradeable by a non-root-account-admin student $ ... get your submission and student ... $ submission.grants_right?(student, :grade) => false Change-Id: Ic3888d216260b26d5b6b75672f83effb442c4836 Reviewed-on: https://gerrit.instructure.com/88000 Tested-by: Jenkins Reviewed-by: Nicholas Pitrak Reviewed-by: Jeremy Neander QA-Review: KC Naegle Product-Review: Christi Wruck --- app/models/grading_period.rb | 4 + app/models/submission.rb | 21 ++++- app/models/user.rb | 6 +- spec/models/grading_period_spec.rb | 34 ++++++++ spec/models/submission_spec.rb | 127 +++++++++++++++++++++++++++++ spec/models/user_spec.rb | 21 +++++ 6 files changed, 211 insertions(+), 2 deletions(-) diff --git a/app/models/grading_period.rb b/app/models/grading_period.rb index 6c74bfe9b27..c22b262db40 100644 --- a/app/models/grading_period.rb +++ b/app/models/grading_period.rb @@ -103,6 +103,10 @@ class GradingPeriod < ActiveRecord::Base end alias_method :is_last, :last? + def closed? + Time.zone.now > close_date + end + def overlapping? overlaps.active.exists? end diff --git a/app/models/submission.rb b/app/models/submission.rb index 06b18ccfaec..cf593ae8e4e 100644 --- a/app/models/submission.rb +++ b/app/models/submission.rb @@ -220,9 +220,17 @@ class Submission < ActiveRecord::Base given { |user| user && user.id == self.user_id && !self.assignment.muted? } can :read_grade - given {|user, session| self.assignment.published? && self.assignment.context.grants_right?(user, session, :manage_grades) } + given {|user, session| !context.feature_enabled?(:multiple_grading_periods) && assignment.published? && context.grants_right?(user, session, :manage_grades) } can :read and can :comment and can :make_group_comment and can :read_grade and can :grade + given {|user, session| context.feature_enabled?(:multiple_grading_periods) && assignment.published? && context.grants_right?(user, session, :manage_grades) } + can :read and can :comment and can :make_group_comment and can :read_grade + + given {|user, session| context.feature_enabled?(:multiple_grading_periods) && + assignment.published? && context.grants_right?(user, session, :manage_grades) && + (user.admin_of_root_account?(assignment.root_account) || !in_closed_grading_period?)} + can :grade + given {|user, session| self.assignment.user_can_read_grades?(user, session) } can :read and can :read_grade @@ -252,6 +260,17 @@ class Submission < ActiveRecord::Base can :view_turnitin_report end + def in_closed_grading_period? + return false unless self.assignment.context.feature_enabled?(:multiple_grading_periods) + + grading_period = GradingPeriod. + for(self.assignment.context). + where(":due_at >= start_date AND :due_at <= end_date", due_at: self.cached_due_date). + first + return false unless grading_period.present? + grading_period.closed? + end + def user_can_read_grade?(user, session=nil) # improves performance by checking permissions on the assignment before the submission self.assignment.user_can_read_grades?(user, session) || self.grants_right?(user, session, :read_grade) diff --git a/app/models/user.rb b/app/models/user.rb index b3599397590..749619d3426 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2310,11 +2310,15 @@ class User < ActiveRecord::Base roles << 'student' unless (enrollment_types & %w[StudentEnrollment StudentViewEnrollment]).empty? roles << 'teacher' unless (enrollment_types & %w[TeacherEnrollment TaEnrollment DesignerEnrollment]).empty? roles << 'observer' unless (enrollment_types & %w[ObserverEnrollment]).empty? - roles << 'admin' unless root_account.all_account_users_for(self).empty? + roles << 'admin' if admin_of_root_account?(root_account) roles end end + def admin_of_root_account?(root_account) + root_account.all_account_users_for(self).any? + end + def eportfolios_enabled? accounts = associated_root_accounts.reject(&:site_admin?) accounts.size == 0 || accounts.any?{ |a| a.settings[:enable_eportfolios] != false } diff --git a/spec/models/grading_period_spec.rb b/spec/models/grading_period_spec.rb index a2417d63f09..413f2a67942 100644 --- a/spec/models/grading_period_spec.rb +++ b/spec/models/grading_period_spec.rb @@ -114,6 +114,40 @@ describe GradingPeriod do end end + describe "#closed?" do + around { |example| Timecop.freeze(now, &example) } + + it "returns true if the current date is past the close date" do + period = grading_period_group.grading_periods.build( + title: "Closed Period", + start_date: 10.days.ago(now), + end_date: 5.days.ago(now), + close_date: 3.days.ago(now) + ) + expect(period).to be_closed + end + + it "returns false if the current date is before the close date" do + period = grading_period_group.grading_periods.build( + title: "Open Period", + start_date: 10.days.ago(now), + end_date: 5.days.ago(now), + close_date: 2.days.from_now(now) + ) + expect(period).not_to be_closed + end + + it "returns false if the current date matches the close date" do + period = grading_period_group.grading_periods.build( + title: "Open Period", + start_date: 10.days.ago(now), + end_date: 5.days.ago(now), + close_date: now + ) + expect(period).not_to be_closed + end + end + describe "#destroy" do before { subject.destroy } diff --git a/spec/models/submission_spec.rb b/spec/models/submission_spec.rb index c2cae0c3213..97b356197ea 100644 --- a/spec/models/submission_spec.rb +++ b/spec/models/submission_spec.rb @@ -35,6 +35,133 @@ describe Submission do } end + describe "Multiple Grading Periods" do + let(:in_closed_grading_period) { 9.days.ago } + let(:in_open_grading_period) { 1.day.from_now } + let(:outside_of_any_grading_period) { 10.days.from_now } + + before(:once) do + @root_account = @context.root_account + @root_account.enable_feature!(:multiple_grading_periods) + group = @root_account.grading_period_groups.create! + @closed_period = group.grading_periods.create!( + title: "Closed!", + start_date: 2.weeks.ago, + end_date: 1.week.ago, + close_date: 3.days.ago + ) + @open_period = group.grading_periods.create!( + title: "Open!", + start_date: 3.days.ago, + end_date: 3.days.from_now, + close_date: 5.days.from_now + ) + group.enrollment_terms << @context.enrollment_term + end + + describe "#in_closed_grading_period?" do + it "returns true if the submission is due in a closed grading period" do + @assignment.due_at = in_closed_grading_period + @assignment.save! + submission = Submission.create!(@valid_attributes) + expect(submission).to be_in_closed_grading_period + end + + it "returns false if the submission is due in an open grading period" do + @assignment.due_at = in_open_grading_period + @assignment.save! + submission = Submission.create!(@valid_attributes) + expect(submission).not_to be_in_closed_grading_period + end + + it "returns false if the submission is due outside of any grading period" do + @assignment.due_at = outside_of_any_grading_period + @assignment.save! + submission = Submission.create!(@valid_attributes) + expect(submission).not_to be_in_closed_grading_period + end + end + + describe "permissions" do + before(:once) do + @admin = user(active_all: true) + @root_account.account_users.create!(user: @admin) + @teacher = user(active_all: true) + @context.enroll_teacher(@teacher) + end + + describe "grade" do + context "the submission is due in a closed grading period" do + before(:once) do + @assignment.due_at = in_closed_grading_period + @assignment.save! + @submission = Submission.create!(@valid_attributes) + end + + it "has grade permissions if the user is a root account admin" do + expect(@submission.grants_right?(@admin, :grade)).to eq(true) + end + + it "does not have grade permissions if the user is not a root account admin" do + expect(@submission.grants_right?(@teacher, :grade)).to eq(false) + end + end + + context "the submission is due in an open grading period" do + before(:once) do + @assignment.due_at = in_open_grading_period + @assignment.save! + @submission = Submission.create!(@valid_attributes) + end + + it "has grade permissions if the user is a root account admin" do + expect(@submission.grants_right?(@admin, :grade)).to eq(true) + end + + it "has grade permissions if the user is non-root account admin with manage_grades permissions" do + expect(@submission.grants_right?(@teacher, :grade)).to eq(true) + end + + it "has not have grade permissions if the user is non-root account admin without manage_grades permissions" do + @student = user(active_all: true) + @context.enroll_student(@student) + expect(@submission.grants_right?(@student, :grade)).to eq(false) + end + end + + context "the submission is due outside of any grading period" do + before(:once) do + @assignment.due_at = outside_of_any_grading_period + @assignment.save! + @submission = Submission.create!(@valid_attributes) + end + + it "has grade permissions if the user is a root account admin" do + expect(@submission.grants_right?(@admin, :grade)).to eq(true) + end + + it "has grade permissions if the user is non-root account admin with manage_grades permissions" do + expect(@submission.grants_right?(@teacher, :grade)).to eq(true) + end + + it "has not have grade permissions if the user is non-root account admin without manage_grades permissions" do + @student = user(active_all: true) + @context.enroll_student(@student) + expect(@submission.grants_right?(@student, :grade)).to eq(false) + end + end + end + end + end + + it "#in_closed_grading_period? returns false if the course does not have Multiple Grading Periods enabled" do + @context.root_account.disable_feature!(:multiple_grading_periods) + @assignment.due_at = 9.days.ago + @assignment.save! + submission = Submission.create!(@valid_attributes) + expect(submission).not_to be_in_closed_grading_period + end + it "should create a new instance given valid attributes" do Submission.create!(@valid_attributes) end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 49f7b362a7a..507eea3605d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2989,6 +2989,27 @@ describe User do end end + describe "#admin_of_root_account?" do + before(:once) do + @user = user(active_all: true) + @root_account = Account.default + end + + it "returns false if the user is not an admin of the root account" do + expect(@user).not_to be_admin_of_root_account(@root_account) + end + + it "returns true if the user is an admin of the root account" do + @root_account.account_users.create!(user: @user) + expect(@user).to be_admin_of_root_account(@root_account) + end + + it "raises an error if the given account is not a root account" do + sub_account = @root_account.sub_accounts.create! + expect { @user.admin_of_root_account?(sub_account) }.to raise_error("must be a root account") + end + end + it "should not grant user_notes rights to restricted users" do course_with_ta(:active_all => true) student_in_course(:course => @course, :active_all => true) From a998025eb2d7382dba13297b9d00c31baf130450 Mon Sep 17 00:00:00 2001 From: Jeremy Neander Date: Thu, 11 Aug 2016 14:37:56 -0500 Subject: [PATCH 05/10] prevent grade change on locked assignments in SRGB closes CNVS-26718 test plan: 1. create or select: a. an account b. an enrollment term for the account c. a grading period set for that term with: * a closed grading period * an open grading period d. a course with that enrollment term e. two students (1 & 2) for the course f. an assignment (A) * due for student 1 in the closed grading period * due for student 2 in the open grading period g. an assignment (B) * due for student 1 after the open grading period * for student 2 without a due date 2. as a Teacher in the course a. visit the screenreader gradebook b. verify grading is locked for student 1 + assignment A c. verify grading is not locked for student 2 + assignment A d. verify grading is not locked for student 1 + assignment B e. verify grading is not locked for student 2 + assignment B 3. as an Admin a. visit the screenreader gradebook b. verify grading is not locked for any student + assignment * Verify grading rules in Grading section * Verify grading rules in Submission Details Modal * Grading includes excusing assignments Change-Id: If382b5ea171bd591cf33ba09082e6c52b8c90cf9 Reviewed-on: https://gerrit.instructure.com/87695 Tested-by: Jenkins Reviewed-by: Spencer Olson Reviewed-by: Derek Bender QA-Review: Amber Taniuchi Product-Review: Christi Wruck --- .../SubmissionDetailsDialog.coffee | 9 ++-- .../components/grading_cell_component.coffee | 6 +-- .../screenreader_gradebook_controller.coffee | 28 +++++++++-- .../tests/components/grading_cell.spec.coffee | 35 ++----------- .../screenreader_gradebook.spec.coffee | 11 ++++ .../tests/shared_ajax_fixtures.coffee | 26 +++++----- app/coffeescripts/gradebook2/Gradebook.coffee | 4 ++ .../gradebook2/GradebookHelpers.coffee | 11 +--- app/jsx/gradebook/SubmissionStateMap.jsx | 6 +-- .../jst/SubmissionDetailsDialog.handlebars | 4 +- .../SubmissionDetailsDialogSpec.coffee | 38 ++------------ .../gradebook2/GradebookHelpersSpec.coffee | 50 +------------------ 12 files changed, 76 insertions(+), 152 deletions(-) diff --git a/app/coffeescripts/SubmissionDetailsDialog.coffee b/app/coffeescripts/SubmissionDetailsDialog.coffee index cc6d0ae5d90..2898aa2c456 100644 --- a/app/coffeescripts/SubmissionDetailsDialog.coffee +++ b/app/coffeescripts/SubmissionDetailsDialog.coffee @@ -23,12 +23,9 @@ define [ else null - isInPastGradingPeriodAndNotAdmin = ((assignment) -> - GradebookHelpers.gradeIsLocked(assignment, ENV) - )(@assignment) - @url = @options.change_grade_url.replace(":assignment", @assignment.id).replace(":submission", @student.id) - @submission = $.extend {}, @student["assignment_#{@assignment.id}"], + submission = @student["assignment_#{@assignment.id}"] + @submission = $.extend {}, submission, label: "student_grading_#{@assignment.id}" inputName: 'submission[posted_grade]' assignment: @assignment @@ -36,7 +33,7 @@ define [ loading: true showPointsPossible: (@assignment.points_possible || @assignment.points_possible == '0') && @assignment.grading_type != "gpa_scale" shouldShowExcusedOption: true - isInPastGradingPeriodAndNotAdmin: isInPastGradingPeriodAndNotAdmin + isInPastGradingPeriodAndNotAdmin: submission.gradeLocked @submission["assignment_grading_type_is_#{@assignment.grading_type}"] = true @submission.grade = "EX" if @submission.excused @$el = $('
') diff --git a/app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee b/app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee index 32d45cbe090..35a2e7c8ed3 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/components/grading_cell_component.coffee @@ -19,9 +19,9 @@ define [ isPercent: Ember.computed.equal('assignment.grading_type', 'percent') isLetterGrade: Ember.computed.equal('assignment.grading_type', 'letter_grade') isPassFail: Ember.computed.equal('assignment.grading_type', 'pass_fail') - isInPastGradingPeriodAndNotAdmin: ( -> - GradebookHelpers.gradeIsLocked(@assignment, ENV) - ).property('assignment') + isInPastGradingPeriodAndNotAdmin: (-> + @submission?.gradeLocked + ).property('submission') nilPointsPossible: Ember.computed.none('assignment.points_possible') isGpaScale: Ember.computed.equal('assignment.grading_type', 'gpa_scale') diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee index 316a3e8bbd4..398691b3a6a 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee @@ -15,8 +15,9 @@ define [ '../../shared/components/ic_submission_download_dialog_component' 'str/htmlEscape' 'compiled/models/grade_summary/CalculationMethodContent' + 'jsx/gradebook/SubmissionStateMap' 'jquery.instructure_date_and_time' - ], (ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, AssignmentDetailsDialog, AssignmentMuter, GradeCalculator, outcomeGrid, ic_submission_download_dialog, htmlEscape, CalculationMethodContent) -> + ], (ajax, round, userSettings, fetchAllPages, parseLinkHeader, I18n, Ember, _, tz, AssignmentDetailsDialog, AssignmentMuter, GradeCalculator, outcomeGrid, ic_submission_download_dialog, htmlEscape, CalculationMethodContent, SubmissionStateMap) -> {get, set, setProperties} = Ember @@ -25,6 +26,9 @@ define [ # http://emberjs.com/api/classes/Ember.ArrayController.html # http://emberjs.com/api/classes/Ember.ObjectController.html + gradingPeriodIsClosed = (gradingPeriod) -> + new Date(gradingPeriod.close_date) < new Date() + studentsUniqByEnrollments = (args...)-> hiddenNameCounter = 1 options = @@ -308,6 +312,17 @@ define [ assignmentSelectDefaultLabel: I18n.t "no_assignment", "No Assignment Selected" outcomeSelectDefaultLabel: I18n.t "no_outcome", "No Outcome Selected" + submissionStateMap: ( + periods = _.map get(window, 'ENV.GRADEBOOK_OPTIONS.active_grading_periods'), (gradingPeriod) => + _.extend({}, gradingPeriod, closed: gradingPeriodIsClosed(gradingPeriod)) + new SubmissionStateMap( + gradingPeriodsEnabled: !!get(window, 'ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled') + selectedGradingPeriodID: '0' + gradingPeriods: periods + isAdmin: ENV.current_user_roles && _.contains(ENV.current_user_roles, "admin") + ) + ) + assignment_groups: [] fetchAssignmentGroups: (-> @@ -607,6 +622,11 @@ define [ assignmentsProxy.addObject as ).observes('assignment_groups', 'assignment_groups.@each') + populateSubmissionStateMap: (-> + return unless @get('enrollments.isLoaded') && @get('assignment_groups.isLoaded') + @submissionStateMap.setup(@get('students').toArray(), @get('assignments').toArray()) + ).observes('enrollments.isLoaded', 'assignment_groups.isLoaded') + includeUngradedAssignments: (-> userSettings.contextGet('include_ungraded_assignments') or false ).property().volatile() @@ -672,18 +692,20 @@ define [ if arguments.length > 1 @set 'selectedStudent', @get('students').findBy('id', selectedSubmission.user_id) @set 'selectedAssignment', @get('assignments').findBy('id', selectedSubmission.assignment_id) - selectedSubmission else return null unless @get('selectedStudent')? and @get('selectedAssignment')? student = @get 'selectedStudent' assignment = @get 'selectedAssignment' sub = get student, "assignment_#{assignment.id}" - sub or { + selectedSubmission = sub or { user_id: student.id assignment_id: assignment.id hidden: !@differentiatedAssignmentVisibleToStudent(assignment, student.id) grade_matches_current_submission: true } + submissionState = @submissionStateMap.getSubmissionState(selectedSubmission) || {} + selectedSubmission.gradeLocked = submissionState.locked + selectedSubmission ).property('selectedStudent', 'selectedAssignment') selectedSubmissionHidden: (-> diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee index 7a257bffbac..08e19e9c6eb 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/components/grading_cell.spec.coffee @@ -29,6 +29,7 @@ define [ run => @submission = Ember.Object.create grade: 'A' + gradeLocked: false assignment_id: 1 user_id: 1 @assignment = Ember.Object.create @@ -48,10 +49,8 @@ define [ test "setting value on init", -> component = App.GradingCellComponent.create() equal(component.get('value'), '-') - equal(@component.get('value'), 'A') - test "saveURL", -> equal(@component.get('saveURL'), "/api/v1/assignment/1/1") @@ -67,36 +66,12 @@ define [ setType 'letter_grade' ok @component.get('isLetterGrade') - test "isInPastGradingPeriodAndNotAdmin is false when multiple grading periods are not enabled", -> - ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled = false - equal @component.get('isInPastGradingPeriodAndNotAdmin'), false - - test "isInPastGradingPeriodAndNotAdmin is false when no grading periods are in the past", -> - ENV.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past = null - equal @component.get('isInPastGradingPeriodAndNotAdmin'), false - - test "isInPastGradingPeriodAndNotAdmin is false when current user roles are undefined", -> - ENV.current_user_roles = null - equal @component.get('isInPastGradingPeriodAndNotAdmin'), false - - test "isInPastGradingPeriodAndNotAdmin is false when the current user is an admin", -> - ENV.current_user_roles = ['admin'] - equal @component.get('isInPastGradingPeriodAndNotAdmin'), false - - test "isInPastGradingPeriodAndNotAdmin is true for assignments in the previous grading period", -> - run => @assignment.set('due_at', tz.parse("2013-10-01T09:59:00Z")) + test "isInPastGradingPeriodAndNotAdmin is true when the submission is gradeLocked", -> + run => @submission.set('gradeLocked', true) equal @component.get('isInPastGradingPeriodAndNotAdmin'), true - test "isInPastGradingPeriodAndNotAdmin is true for assignments due exactly at the end of the previous grading period", -> - run => @assignment.set('due_at', tz.parse("2013-10-01T10:00:00Z")) - equal @component.get('isInPastGradingPeriodAndNotAdmin'), true - - test "isInPastGradingPeriodAndNotAdmin is false for assignments after the previous grading period", -> - run => @assignment.set('due_at', tz.parse("2013-10-01T10:01:00Z")) - equal @component.get('isInPastGradingPeriodAndNotAdmin'), false - - test "isInPastGradingPeriodAndNotAdmin is false for assignments without a due date", -> - run => @assignment.set('due_at', null) + test "isInPastGradingPeriodAndNotAdmin is false when the submission is not gradeLocked", -> + run => @submission.set('gradeLocked', false) equal @component.get('isInPastGradingPeriodAndNotAdmin'), false test "nilPointsPossible", -> diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee index bc771d91ec7..fb05d3c6e5e 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/controllers/screenreader_gradebook.spec.coffee @@ -194,6 +194,17 @@ define [ _.each submission, (val, key) => equal selectedSubmission[key], val, "#{key} is the expected value on selectedSubmission" + test 'selectedSubmission sets gradeLocked', -> + selectedSubmission = @srgb.get('selectedSubmission') + equal selectedSubmission.gradeLocked, false + + test 'selectedSubmission sets gradeLocked for unassigned students', -> + @student = @srgb.get('students')[1] + Ember.run => + @srgb.set('selectedStudent', @student) + selectedSubmission = @srgb.get('selectedSubmission') + equal selectedSubmission.gradeLocked, true + module 'screenreader_gradebook_controller: with selected assignment', setup: -> setup.call this diff --git a/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.coffee b/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.coffee index b729d02f68a..24fb3e0c274 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/tests/shared_ajax_fixtures.coffee @@ -247,69 +247,69 @@ define [ students = [ { user_id: '1' - user: { id: '1', name: 'Bob' } + user: { id: '1', name: 'Bob', group_ids: [], sections: [] } course_section_id: '1' } { user_id: '2' - user: { id: '2', name: 'Fred' } + user: { id: '2', name: 'Fred', group_ids: [], sections: [] } course_section_id: '1' } { user_id: '3' - user: { id: '3', name: 'Suzy' } + user: { id: '3', name: 'Suzy', group_ids: [], sections: [] } course_section_id: '1' } { user_id: '4' - user: { id: '4', name: 'Buffy' } + user: { id: '4', name: 'Buffy', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '5' - user: { id: '5', name: 'Willow' } + user: { id: '5', name: 'Willow', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '5' - user: { id: '5', name: 'Willow' } + user: { id: '5', name: 'Willow', group_ids: [], sections: [] } course_section_id: '1' } { user_id: '6' - user: { id: '6', name: 'Giles' } + user: { id: '6', name: 'Giles', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '7' - user: { id: '7', name: 'Xander' } + user: { id: '7', name: 'Xander', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '8' - user: { id: '8', name: 'Cordelia' } + user: { id: '8', name: 'Cordelia', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '9' - user: { id: '9', name: 'Drusilla' } + user: { id: '9', name: 'Drusilla', group_ids: [], sections: [] } course_section_id: '1' } { user_id: '10' - user: { id: '10', name: 'Spike' } + user: { id: '10', name: 'Spike', group_ids: [], sections: [] } course_section_id: '2' } { user_id: '10' - user: { id: '10', name: 'Spike' } + user: { id: '10', name: 'Spike', group_ids: [], sections: [] } course_section_id: '1' } ] concludedStudents = [ { - user: { id: '105', name: 'Lyra' } + user: { id: '105', name: 'Lyra', group_ids: [], sections: [] } course_section_id: '1' user_id: '105' workflow_state: 'completed' diff --git a/app/coffeescripts/gradebook2/Gradebook.coffee b/app/coffeescripts/gradebook2/Gradebook.coffee index c16b2af3e03..a0d45cb018d 100644 --- a/app/coffeescripts/gradebook2/Gradebook.coffee +++ b/app/coffeescripts/gradebook2/Gradebook.coffee @@ -291,6 +291,8 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> for assignment_id of @assignments student["assignment_#{assignment_id}"] ?= @submissionStateMap.getSubmission student.id, assignment_id + submissionState = @submissionStateMap.getSubmissionState(student["assignment_#{assignment_id}"]) + student["assignment_#{assignment_id}"].gradeLocked = submissionState.locked student.initialized = true @calculateStudentGrade(student) @@ -606,6 +608,8 @@ SectionMenuView, GradingPeriodMenuView, GradebookKeyboardNav, ColumnArranger) -> @updateAssignmentVisibilities(submission) unless submission.assignment_visible @updateSubmission(submission) @submissionStateMap.setSubmissionCellState(student, @assignments[submission.assignment_id], submission) + submissionState = @submissionStateMap.getSubmissionState(submission) + student["assignment_#{submission.assignment_id}"].gradeLocked = submissionState.locked @calculateStudentGrade(student) @grid.updateCell student.row, cell unless thisCellIsActive @updateRowTotals student.row diff --git a/app/coffeescripts/gradebook2/GradebookHelpers.coffee b/app/coffeescripts/gradebook2/GradebookHelpers.coffee index ec3fc84a394..52c19a87e26 100644 --- a/app/coffeescripts/gradebook2/GradebookHelpers.coffee +++ b/app/coffeescripts/gradebook2/GradebookHelpers.coffee @@ -1,9 +1,8 @@ define [ 'jquery' - 'underscore' 'i18n!gradebook2' 'jsx/gradebook/grid/constants' -], ($, _, I18n, GradebookConstants) -> +], ($, I18n, GradebookConstants) -> FLASH_ERROR_CLASS: '.ic-flash-error' flashMaxLengthError: () -> @@ -23,11 +22,3 @@ define [ textareaIsLessThanOrEqualToMaxLength: (textareaLength) -> textareaLength <= GradebookConstants.MAX_NOTE_LENGTH - - gradeIsLocked: (assignment, env) -> - return false unless env.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled - return false unless env.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past - return false unless env.current_user_roles - return false if _.contains(env.current_user_roles, "admin") - latest_end_date = new Date(env.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past) - assignment.due_at != null && assignment.due_at <= latest_end_date diff --git a/app/jsx/gradebook/SubmissionStateMap.jsx b/app/jsx/gradebook/SubmissionStateMap.jsx index d214f61c588..9632e636478 100644 --- a/app/jsx/gradebook/SubmissionStateMap.jsx +++ b/app/jsx/gradebook/SubmissionStateMap.jsx @@ -172,13 +172,13 @@ define([ } getSubmission(user_id, assignment_id) { - return this.submissionMap[user_id][assignment_id]; + return (this.submissionMap[user_id] || {})[assignment_id]; } getSubmissionState({ user_id, assignment_id }) { - return this.submissionCellMap[user_id][assignment_id]; + return (this.submissionCellMap[user_id] || {})[assignment_id]; } }; return SubmissionState; -}) \ No newline at end of file +}) diff --git a/app/views/jst/SubmissionDetailsDialog.handlebars b/app/views/jst/SubmissionDetailsDialog.handlebars index bac23d0568b..50796f9f566 100644 --- a/app/views/jst/SubmissionDetailsDialog.handlebars +++ b/app/views/jst/SubmissionDetailsDialog.handlebars @@ -4,7 +4,9 @@
{{> grading_box}} - + {{#unless isInPastGradingPeriodAndNotAdmin}} + + {{/unless}}
{{#if speedGraderUrl }} {{#t "more_details_in_the_speedgrader"}}More details in the SpeedGrader{{/t}} diff --git a/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee b/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee index 64e0bc906be..b7960fdb638 100644 --- a/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee +++ b/spec/coffeescripts/gradebook/SubmissionDetailsDialogSpec.coffee @@ -112,42 +112,12 @@ define [ excusedOptionText = $('.grading_value option')[3].text deepEqual excusedOptionText, 'Excused' - test "is enabled when multiple grading periods are not enabled", -> - ENV.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled = false - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), false - - test "is enabled when no grading periods are in the past", -> - ENV.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past = null - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), false - - test "is enabled when current user roles are undefined", -> - ENV.current_user_roles = null - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), false - - test "is enabled when the current user is an admin", -> - ENV.current_user_roles = ['admin'] - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), false - - test "is disabled for assignments in the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T09:59:00Z") + test "is disabled for assignments locked for the given student", -> + @user.assignment_1.gradeLocked = true new SubmissionDetailsDialog(@assignment, @user, @options).open() equal $('#student_grading_1').prop('disabled'), true - test "is disabled for assignments due exactly at the end of the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T10:00:00Z") - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), true - - test "is enabled for assignments after the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T10:01:00Z") - new SubmissionDetailsDialog(@assignment, @user, @options).open() - equal $('#student_grading_1').prop('disabled'), false - - test "is enabled for assignments without a due date", -> - @assignment.due_at = null + test "is enabled for assignments not locked for the given student", -> + @user.assignment_1.gradeLocked = false new SubmissionDetailsDialog(@assignment, @user, @options).open() equal $('#student_grading_1').prop('disabled'), false diff --git a/spec/coffeescripts/gradebook2/GradebookHelpersSpec.coffee b/spec/coffeescripts/gradebook2/GradebookHelpersSpec.coffee index 035028e46c6..57795da49fd 100644 --- a/spec/coffeescripts/gradebook2/GradebookHelpersSpec.coffee +++ b/spec/coffeescripts/gradebook2/GradebookHelpersSpec.coffee @@ -1,10 +1,7 @@ define [ 'compiled/gradebook2/GradebookHelpers' 'jsx/gradebook/grid/constants' - 'timezone' - 'compiled/models/Assignment' -], (GradebookHelpers, GradebookConstants, tz, Assignment) -> - +], (GradebookHelpers, GradebookConstants) -> module "GradebookHelpers#noErrorsOnPage", setup: -> @mockFind = @mock($, "find") @@ -42,48 +39,3 @@ define [ "the max allowed length AND there are no DOM errors", -> @mockFind.expects("find").once().returns([]) ok GradebookHelpers.maxLengthErrorShouldBeShown(GradebookConstants.MAX_NOTE_LENGTH + 1) - - module "GradebookHelpers#gradeIsLocked", - setup: -> - @env = - current_user_roles: [] - GRADEBOOK_OPTIONS: - multiple_grading_periods_enabled: true - latest_end_date_of_admin_created_grading_periods_in_the_past: "2013-10-01T10:00:00Z" - @assignment = new Assignment(id: 1) - @assignment.due_at = tz.parse("2013-10-01T10:00:00Z") - @assignment.grading_type = 'points' - - test "gradeIsLocked is false when multiple grading periods are not enabled", -> - @env.GRADEBOOK_OPTIONS.multiple_grading_periods_enabled = false - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - - test "gradeIsLocked is false when no grading periods are in the past", -> - @env.GRADEBOOK_OPTIONS.latest_end_date_of_admin_created_grading_periods_in_the_past = null - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - - test "gradeIsLocked is false when current user roles are undefined", -> - @env.current_user_roles = null - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - @env.current_user_roles = undefined - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - - test "gradeIsLocked is false when the current user is an admin", -> - @env.current_user_roles = ['admin'] - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - - test "gradeIsLocked is true for assignments in the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T09:59:00Z") - equal GradebookHelpers.gradeIsLocked(@assignment, @env), true - - test "gradeIsLocked is true for assignments due exactly at the end of the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T10:00:00Z") - equal GradebookHelpers.gradeIsLocked(@assignment, @env), true - - test "gradeIsLocked is false for assignments after the previous grading period", -> - @assignment.due_at = tz.parse("2013-10-01T10:01:00Z") - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false - - test "gradeIsLocked is false for assignments without a due date", -> - @assignment.due_at = null - equal GradebookHelpers.gradeIsLocked(@assignment, @env), false From 3e8f84e3980372b3a327843eca73be8a8ac8ff62 Mon Sep 17 00:00:00 2001 From: Cameron Matheson Date: Thu, 11 Aug 2016 16:14:49 -0600 Subject: [PATCH 06/10] speedgrader: fix grade disabling fixes CNVS-30961 Test plan: * view a quiz submission in speedgrader - the grade input in the sidebar should be disabled (you have to change the score via fudge points in the content area to change the grade) * the grade input should be disabled wherever appropriate for moderated grading too Change-Id: I97a9e5395eb00e206f8e8c2bde8dc5d7e53df511 Reviewed-on: https://gerrit.instructure.com/88589 Tested-by: Jenkins Reviewed-by: Keith T. Garner QA-Review: Alex Morris Product-Review: Cameron Matheson --- public/javascripts/speed_grader.js | 24 +++++++++---------- .../speedgrader_quiz_submissions_spec.rb | 5 +++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/public/javascripts/speed_grader.js b/public/javascripts/speed_grader.js index 8b3ee9cb196..caa23d6f296 100644 --- a/public/javascripts/speed_grader.js +++ b/public/javascripts/speed_grader.js @@ -1506,15 +1506,14 @@ define([ var $submission_to_view = $("#submission_to_view"), submissionToViewVal = $submission_to_view.val(), currentSelectedIndex = currentIndex(this, submissionToViewVal), - isMostRecent = this.currentStudent && - this.currentStudent.submission && - this.currentStudent.submission.submission_history && - this.currentStudent.submission.submission_history.length - 1 === currentSelectedIndex, - submission = this.currentStudent && - this.currentStudent.submission && - this.currentStudent.submission.submission_history && - this.currentStudent.submission.submission_history[currentSelectedIndex] && - this.currentStudent.submission.submission_history[currentSelectedIndex].submission + submissionHolder = this.currentStudent && + this.currentStudent.submission, + submissionHistory = submissionHolder && submissionHolder.submission_history, + isMostRecent = submissionHistory && + submissionHistory.length - 1 === currentSelectedIndex, + submission = submissionHistory && + submissionHistory[currentSelectedIndex] && + submissionHistory[currentSelectedIndex].submission || {}, inlineableAttachments = [], browserableAttachments = []; @@ -1613,6 +1612,10 @@ define([ var isConcluded = EG.isStudentConcluded(this.currentStudent.id); $enrollment_concluded_notice.showIf(isConcluded); SpeedgraderHelpers.setRightBarDisabled(isConcluded); + EG.setGradeReadOnly((typeof submissionHolder != "undefined" && + submissionHolder.submission_type === "online_quiz") || + isConcluded); + if (isConcluded) { $full_width_container.addClass("with_enrollment_notice"); } @@ -2204,9 +2207,6 @@ define([ var grade = EG.getGradeToShow(submission, ENV.grading_role); $grade.val(grade); - EG.setGradeReadOnly((typeof submission != "undefined" && - submission.submission_type === 'online_quiz') || - EG.isStudentConcluded(EG.currentStudent.id)); $('#submit_same_score').hide(); if (typeof submission != "undefined" && submission.score !== null) { diff --git a/spec/selenium/speedgrader_quiz_submissions_spec.rb b/spec/selenium/speedgrader_quiz_submissions_spec.rb index 89f39385b74..127cff18256 100644 --- a/spec/selenium/speedgrader_quiz_submissions_spec.rb +++ b/spec/selenium/speedgrader_quiz_submissions_spec.rb @@ -130,12 +130,15 @@ describe "speed grader - quiz submissions" do Quizzes::SubmissionGrader.new(qs).grade_submission get "/courses/#{@course.id}/gradebook/speed_grader?assignment_id=#{@assignment.id}" + + input = f('#grade_container input') + expect(input["readonly"]).to eq "true" + in_frame('speedgrader_iframe') do question_inputs = ff('.header .question_input') question_inputs.each { |qi| replace_content(qi, 3) } submit_form('#update_history_form') end - input = f('#grade_container input') expect(input).to have_attribute('value', expected_points) end From 2eeaf1f77489e7efdbfd89700ff0d07010d51806 Mon Sep 17 00:00:00 2001 From: Spencer Olson Date: Tue, 23 Aug 2016 11:00:15 -0500 Subject: [PATCH 07/10] gradebook: hide comment bubble when locked for close date when a submission cell in the gradebook is locked due to falling in a closed grading period, the 'comment bubble' is no longer available within the cell. closes CNVS-31205 test plan: 1. sign in as a non-admin teacher in a course with Multiple Grading Periods enabled and at least 1 closed grading period. 2. create an assignment due for everyone in the closed grading period. 3. go to the gradebook and select the closed grading period from the dropdown. 4. verify all the submission cells for the assignment: a) are locked, b) have a tooltip stating the submission falls in a closed grading period, and c) do not contain a clickable 'comment bubble' in the cell. Change-Id: I54d1638a6829c92a5a8dc786d62b11a386e466e8 Reviewed-on: https://gerrit.instructure.com/88500 Tested-by: Jenkins Reviewed-by: Jeremy Neander Reviewed-by: Keith T. Garner Reviewed-by: Derek Bender QA-Review: Alex Morris Product-Review: Keith T. Garner --- app/coffeescripts/gradebook2/SubmissionCell.coffee | 2 +- spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/coffeescripts/gradebook2/SubmissionCell.coffee b/app/coffeescripts/gradebook2/SubmissionCell.coffee index 18c2ca7791d..58d729a34a3 100644 --- a/app/coffeescripts/gradebook2/SubmissionCell.coffee +++ b/app/coffeescripts/gradebook2/SubmissionCell.coffee @@ -123,7 +123,7 @@ define [ tooltipText = $.map(specialClasses, (c)-> GRADEBOOK_TRANSLATIONS["submission_tooltip_#{c}"]).join ', ' - cellCommentHTML = if !opts.student.isConcluded + cellCommentHTML = if !opts.student.isConcluded && !opts.isLocked """ submission comments """ diff --git a/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee b/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee index 392faf6aaaa..d8350692b5f 100644 --- a/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee +++ b/spec/coffeescripts/gradebook2/SubmissionCellSpec.coffee @@ -67,6 +67,10 @@ define [ submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: true }) ok submissionCellResponse.indexOf("cannot_edit") > -1 + test "#class.formatter, isLocked: true does not include the cell comment bubble", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: true }) + equal submissionCellResponse.indexOf("gradebook-cell-comment"), -1 + test "#class.formatter, isLocked: false doesn't add grayed-out", -> submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: false }) equal submissionCellResponse.indexOf("grayed-out"), -1 @@ -75,6 +79,10 @@ define [ submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: false }) equal submissionCellResponse.indexOf("cannot_edit"), -1 + test "#class.formatter, isLocked: false includes the cell comment bubble", -> + submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { isLocked: false }) + ok submissionCellResponse.indexOf("gradebook-cell-comment") > -1 + test "#class.formatter, tooltip adds your text to the special classes", -> submissionCellResponse = SubmissionCell.formatter(0, 0, { grade: 73 }, {}, {}, { tooltip: "dora_the_explorer" }) ok submissionCellResponse.indexOf("dora_the_explorer") > -1 From 2062cad323ad3607a5125993b805254c1317e926 Mon Sep 17 00:00:00 2001 From: Jeremy Neander Date: Mon, 15 Aug 2016 13:14:00 -0500 Subject: [PATCH 08/10] remove assignment group weights modal in gradebook closes CNVS-30893 test plan: * verify that the assignment group weights modal cannot be accessed from gradebook Change-Id: I08339aa5de1958d64ed95f8385d19d31a4feb3db Reviewed-on: https://gerrit.instructure.com/87900 Tested-by: Jenkins Reviewed-by: Spencer Olson Reviewed-by: Derek Bender QA-Review: KC Naegle Product-Review: Christi Wruck --- .../screenreader_gradebook_controller.coffee | 11 +--- .../assignment_toggles_and_actions.hbs | 11 ---- .../screenreader_gradebook.spec.coffee | 11 ---- .../views/assignments_view.coffee | 17 +------ app/coffeescripts/gradebook2/Gradebook.coffee | 2 +- app/views/gradebooks/gradebook2.html.erb | 3 -- .../gradebook2/gradebook2_a11y_spec.rb | 7 --- .../gradebook2_group_weight_spec.rb | 49 +++++------------- .../gradebook2/gradebook2_srgb_spec.rb | 26 ---------- ...radebook2_srgb_student_information_spec.rb | 8 +-- .../gradebook2_total_points_toggle_spec.rb | 14 +++-- spec/selenium/helpers/gradebook2_common.rb | 51 ------------------- .../helpers/gradebook2_srgb_common.rb | 7 --- 13 files changed, 30 insertions(+), 187 deletions(-) diff --git a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee index eeb8d3f9441..c6794b58d11 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee +++ b/app/coffeescripts/ember/screenreader_gradebook/controllers/screenreader_gradebook_controller.coffee @@ -238,23 +238,14 @@ define [ Ember.$.subscribe 'submissions_updated', _.bind(@updateSubmissionsFromExternal, this) ).on('init') - setupAssignmentGroupsChange: (-> - Ember.$.subscribe 'assignment_group_weights_changed', _.bind(@checkWeightingScheme, this) + setupAssignmentWeightingScheme: (-> @set 'weightingScheme', ENV.GRADEBOOK_OPTIONS.group_weighting_scheme ).on('init') willDestroy: -> Ember.$.unsubscribe 'submissions_updated' - Ember.$.unsubscribe 'assignment_group_weights_changed' @_super() - checkWeightingScheme: ({assignmentGroups})-> - ags = @get('assignment_groups') - ags.clear() - ags.pushObjects assignmentGroups - - @set 'weightingScheme', ENV.GRADEBOOK_OPTIONS.group_weighting_scheme - updateSubmissionsFromExternal: (submissions) -> subs_proxy = @get('submissions') selected = @get('selectedSubmission') diff --git a/app/coffeescripts/ember/screenreader_gradebook/templates/settings/assignment_toggles_and_actions.hbs b/app/coffeescripts/ember/screenreader_gradebook/templates/settings/assignment_toggles_and_actions.hbs index db337e41f0b..0603c8235bc 100644 --- a/app/coffeescripts/ember/screenreader_gradebook/templates/settings/assignment_toggles_and_actions.hbs +++ b/app/coffeescripts/ember/screenreader_gradebook/templates/settings/assignment_toggles_and_actions.hbs @@ -73,17 +73,6 @@ {{!-- Intentionally left empty so this scales to smaller screens --}}
-
- -
-
<% end -%> -