diff --git a/cms/djangoapps/contentstore/features/grading.feature b/cms/djangoapps/contentstore/features/grading.feature index 0e0ce561ee..5cf5a72866 100644 --- a/cms/djangoapps/contentstore/features/grading.feature +++ b/cms/djangoapps/contentstore/features/grading.feature @@ -99,3 +99,21 @@ Feature: Course Grading And I have populated the course And I am viewing the grading settings Then I cannot edit the "Fail" grade range + + Scenario: User can set a grace period greater than one day + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I change the grace period to "48:00" + And I press the "Save" notification button + And I reload the page + Then I see the grace period is "48:00" + + Scenario: Grace periods of more than 59 minutes are wrapped to the correct time + Given I have opened a new course in Studio + And I have populated the course + And I am viewing the grading settings + When I change the grace period to "01:99" + And I press the "Save" notification button + And I reload the page + Then I see the grace period is "02:39" diff --git a/cms/djangoapps/contentstore/features/grading.py b/cms/djangoapps/contentstore/features/grading.py index bedab86bd9..719b3f7f7c 100644 --- a/cms/djangoapps/contentstore/features/grading.py +++ b/cms/djangoapps/contentstore/features/grading.py @@ -143,6 +143,21 @@ def cannot_edit_fail(_step): except InvalidElementStateException: pass # We should get this exception on failing to edit the element + +@step(u'I change the grace period to "(.*)"$') +def i_change_grace_period(_step, grace_period): + grace_period_css = '#course-grading-graceperiod' + ele = world.css_find(grace_period_css).first + ele.value = grace_period + + +@step(u'I see the grace period is "(.*)"$') +def the_grace_period_is(_step, grace_period): + grace_period_css = '#course-grading-graceperiod' + ele = world.css_find(grace_period_css).first + assert ele.value == grace_period + + def get_type_index(name): name_id = '#course-grading-assignment-name' all_types = world.css_find(name_id) diff --git a/cms/envs/common.py b/cms/envs/common.py index b89bdafdc4..29e99b2551 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -246,7 +246,8 @@ PIPELINE_JS = { 'js/models/metadata_model.js', 'js/views/metadata_editor_view.js', 'js/models/uploads.js', 'js/views/uploads.js', 'js/models/textbook.js', 'js/views/textbook.js', - 'js/views/assets.js', 'js/utility.js'], + 'js/views/assets.js', 'js/utility.js', + 'js/models/settings/course_grading_policy.js'], 'output_filename': 'js/cms-application.js', 'test_order': 0 }, diff --git a/cms/static/coffee/spec/models/settings_grading_spec.coffee b/cms/static/coffee/spec/models/settings_grading_spec.coffee new file mode 100644 index 0000000000..a8066d21ae --- /dev/null +++ b/cms/static/coffee/spec/models/settings_grading_spec.coffee @@ -0,0 +1,24 @@ +describe "CMS.Models.Settings.CourseGradingPolicy", -> + beforeEach -> + @model = new CMS.Models.Settings.CourseGradingPolicy() + + describe "parse", -> + it "sets a null grace period to 00:00", -> + attrs = @model.parse(grace_period: null) + expect(attrs.grace_period).toEqual( + hours: 0, + minutes: 0 + ) + + describe "parseGracePeriod", -> + it "parses a time in HH:MM format", -> + time = @model.parseGracePeriod("07:19") + expect(time).toEqual( + hours: 7, + minutes: 19 + ) + + it "returns null on an incorrectly formatted string", -> + expect(@model.parseGracePeriod("asdf")).toBe(null) + expect(@model.parseGracePeriod("7:19")).toBe(null) + expect(@model.parseGracePeriod("1000:00")).toBe(null) diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index 04ae3f4c32..99b0b52a19 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -24,6 +24,14 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({ } attributes.graders = graderCollection; } + // If grace period is unset or equal to 00:00 on the server, + // it's received as null + if (attributes['grace_period'] === null) { + attributes.grace_period = { + hours: 0, + minutes: 0 + } + } return attributes; }, url : function() { @@ -44,8 +52,25 @@ CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({ return newDate; }, - dateToGracePeriod : function(date) { - return {hours : date.getHours(), minutes : date.getMinutes(), seconds : date.getSeconds() }; + parseGracePeriod : function(grace_period) { + // Enforce hours:minutes format + if(!/^\d{2,3}:\d{2}$/.test(grace_period)) { + return null; + } + var pieces = grace_period.split(/:/); + return { + hours: parseInt(pieces[0], 10), + minutes: parseInt(pieces[1], 10) + } + }, + validate : function(attrs) { + if(_.has(attrs, 'grace_period')) { + if(attrs['grace_period'] === null) { + return { + 'grace_period': gettext('Grace period must be specified in HH:MM format.') + } + } + } } }); diff --git a/cms/static/js/views/settings/settings_grading_view.js b/cms/static/js/views/settings/settings_grading_view.js index b6fac04899..1a4235e438 100644 --- a/cms/static/js/views/settings/settings_grading_view.js +++ b/cms/static/js/views/settings/settings_grading_view.js @@ -28,9 +28,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ this.setupCutoffs(); - // Instrument grace period - this.$el.find('#course-grading-graceperiod').timepicker(); - // instantiates an editor template for each update in the collection // Because this calls render, put it after everything which render may depend upon to prevent race condition. window.templateLoader.loadRemoteTemplate("course_grade_policy", @@ -51,6 +48,10 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ // prevent bootstrap race condition by event dispatch if (!this.template) return; + this.clearValidationErrors(); + + this.renderGracePeriod(); + // Create and render the grading type subs var self = this; var gradelist = this.$el.find('.course-grading-assignment-list'); @@ -87,13 +88,6 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ // render the grade cutoffs this.renderCutoffBar(); - var graceEle = this.$el.find('#course-grading-graceperiod'); - graceEle.timepicker({'timeFormat' : 'H:i'}); // init doesn't take setTime - if (this.model.has('grace_period')) graceEle.timepicker('setTime', this.model.gracePeriodToDate()); - // remove any existing listeners to keep them from piling on b/c render gets called frequently - graceEle.off('change', this.setGracePeriod); - graceEle.on('change', this, this.setGracePeriod); - return this; }, addAssignmentType : function(e) { @@ -103,17 +97,26 @@ CMS.Views.Settings.Grading = CMS.Views.ValidatingView.extend({ fieldToSelectorMap : { 'grace_period' : 'course-grading-graceperiod' }, + renderGracePeriod: function() { + var format = function(time) { + return time >= 10 ? time.toString() : '0' + time; + }; + var grace_period = this.model.get('grace_period'); + this.$el.find('#course-grading-graceperiod').val( + format(grace_period.hours) + ':' + format(grace_period.minutes) + ); + }, setGracePeriod : function(event) { - var self = event.data; - self.clearValidationErrors(); - var newVal = self.model.dateToGracePeriod($(event.currentTarget).timepicker('getTime')); - self.model.set('grace_period', newVal, {validate: true}); + this.clearValidationErrors(); + var newVal = this.model.parseGracePeriod($(event.currentTarget).val()); + this.model.set('grace_period', newVal, {validate: true}); }, updateModel : function(event) { if (!this.selectorToField[event.currentTarget.id]) return; switch (this.selectorToField[event.currentTarget.id]) { - case 'grace_period': // handled above + case 'grace_period': + this.setGracePeriod(event); break; default: diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index f3a4584a26..6dd22482ec 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -97,7 +97,7 @@ from contentstore import utils