diff --git a/cms/djangoapps/contentstore/utils.py b/cms/djangoapps/contentstore/utils.py index 7791bb681238..8be19ebc7ba2 100644 --- a/cms/djangoapps/contentstore/utils.py +++ b/cms/djangoapps/contentstore/utils.py @@ -2,6 +2,7 @@ Common utility functions useful throughout the contentstore """ +from collections import defaultdict import logging from contextlib import contextmanager from datetime import datetime @@ -731,3 +732,24 @@ def translation_language(language): translation.activate(previous) else: yield + + +def get_subsections_by_assignment_type(course_key): + """ + Construct a dictionary mapping each found assignment type in the course + to a list of dictionaries with the display name of the subsection and + the display name of the section they are in + """ + subsections_by_assignment_type = defaultdict(list) + + with modulestore().bulk_operations(course_key): + course = modulestore().get_course(course_key, depth=3) + sections = course.get_children() + for section in sections: + subsections = section.get_children() + for subsection in subsections: + if subsection.format: + subsections_by_assignment_type[subsection.format].append( + f'{section.display_name} - {subsection.display_name}' + ) + return subsections_by_assignment_type diff --git a/cms/djangoapps/contentstore/views/course.py b/cms/djangoapps/contentstore/views/course.py index 3bf9ade016e4..26e54e959874 100644 --- a/cms/djangoapps/contentstore/views/course.py +++ b/cms/djangoapps/contentstore/views/course.py @@ -100,6 +100,7 @@ add_instructor, get_lms_link_for_item, get_proctored_exam_settings_url, + get_subsections_by_assignment_type, initialize_permissions, remove_all_instructors, reverse_course_url, @@ -1343,6 +1344,7 @@ def grading_handler(request, course_key_string, grader_index=None): if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': course_details = CourseGradingModel.fetch(course_key) + course_assignment_lists = get_subsections_by_assignment_type(course_key) return render_to_response('settings_graders.html', { 'context_course': course_block, 'course_locator': course_key, @@ -1350,6 +1352,7 @@ def grading_handler(request, course_key_string, grader_index=None): 'grading_url': reverse_course_url('grading_handler', course_key), 'is_credit_course': is_credit_course(course_key), 'mfe_proctored_exam_settings_url': get_proctored_exam_settings_url(course_block.id), + 'course_assignment_lists': dict(course_assignment_lists) }) elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): if request.method == 'GET': diff --git a/cms/static/js/factories/settings_graders.js b/cms/static/js/factories/settings_graders.js index 2be17111e0e2..a78235e5cd49 100644 --- a/cms/static/js/factories/settings_graders.js +++ b/cms/static/js/factories/settings_graders.js @@ -2,7 +2,7 @@ define([ 'jquery', 'js/views/settings/grading', 'js/models/settings/course_grading_policy' ], function($, GradingView, CourseGradingPolicyModel) { 'use strict'; - return function(courseDetails, gradingUrl) { + return function(courseDetails, gradingUrl, courseAssignmentLists) { var model, editor; $('form :input') @@ -17,7 +17,8 @@ define([ model.urlRoot = gradingUrl; editor = new GradingView({ el: $('.settings-grading'), - model: model + model: model, + courseAssignmentLists: courseAssignmentLists }); editor.render(); }; diff --git a/cms/static/js/models/settings/course_grading_policy.js b/cms/static/js/models/settings/course_grading_policy.js index 316e35a8d2a5..adfa8aac2a64 100644 --- a/cms/static/js/models/settings/course_grading_policy.js +++ b/cms/static/js/models/settings/course_grading_policy.js @@ -7,7 +7,9 @@ define(['backbone', 'js/models/location', 'js/collections/course_grader', 'edx-u graders: null, // CourseGraderCollection grade_cutoffs: null, // CourseGradeCutoff model grace_period: null, // either null or { hours: n, minutes: m, ...} - minimum_grade_credit: null // either null or percentage + minimum_grade_credit: null, // either null or percentage + assignment_count_info: [], // Object with keys mapping assignment type names to a list of + //assignment display names }, parse: function(attributes) { if (attributes.graders) { diff --git a/cms/static/js/views/settings/grading.js b/cms/static/js/views/settings/grading.js index 4320204035ac..599d66f4f410 100644 --- a/cms/static/js/views/settings/grading.js +++ b/cms/static/js/views/settings/grading.js @@ -24,7 +24,7 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { 'focus :input': 'inputFocus', 'blur :input': 'inputUnfocus' }, - initialize: function() { + initialize: function(options) { // load template for grading view var self = this; this.template = HtmlUtils.template( @@ -40,6 +40,7 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { this.model.get('graders').on('reset', this.render, this); this.model.get('graders').on('add', this.render, this); this.selectorToField = _.invert(this.fieldToSelectorMap); + this.courseAssignmentLists = options.courseAssignmentLists; this.render(); }, @@ -73,10 +74,26 @@ function(ValidatingView, _, $, ui, GraderView, StringUtils, HtmlUtils) { }, this); gradeCollection.each(function(gradeModel) { - HtmlUtils.append(gradelist, self.template({model: gradeModel})); + var graderType = gradeModel.get('type'); + var graderTypeAssignmentList = self.courseAssignmentLists[graderType] + if (graderTypeAssignmentList === undefined) { + graderTypeAssignmentList = []; + } + + HtmlUtils.append( + gradelist, + self.template({ + model: gradeModel, + assignmentList: graderTypeAssignmentList + }) + ); var newEle = gradelist.children().last(); - var newView = new GraderView({el: newEle, - model: gradeModel, collection: gradeCollection}); + var newView = new GraderView({ + el: newEle, + model: gradeModel, + collection: gradeCollection, + courseAssignmentCountInfo: self.courseAssignmentCountInfo, + }); // Listen in order to rerender when the 'cancel' button is // pressed self.listenTo(newView, 'revert', _.bind(self.render, self)); diff --git a/cms/static/sass/views/_settings.scss b/cms/static/sass/views/_settings.scss index 10d2ccc3272a..cc62fd436f0d 100644 --- a/cms/static/sass/views/_settings.scss +++ b/cms/static/sass/views/_settings.scss @@ -882,6 +882,38 @@ #field-course-grading-assignment-droppable { width: flex-grid(2, 6); } + + .assignment-count-info { + padding: $baseline; + border-radius: 3px; + @extend %t-copy-sub2; + } + + .assignment-count-warning { + background-color: $yellow-l4; + + .assignment-count-warning-header{ + @extend %t-copy-sub1; + font-weight: bold; + .header-warning { + font-weight: bolder; + } + margin-bottom: ($baseline / 2); + } + + .assignment-count-warning-content { + + ol.assignment_type_count_list { + list-style: auto; + list-style-position: inside; + padding-left: ($baseline*1.5); + } + } + } + + .assignment-count-matches { + background-color: $green-l4; + } } .actions { diff --git a/cms/templates/js/course_grade_policy.underscore b/cms/templates/js/course_grade_policy.underscore index ac59928949b0..a787978a5a03 100644 --- a/cms/templates/js/course_grade_policy.underscore +++ b/cms/templates/js/course_grade_policy.underscore @@ -29,6 +29,53 @@ <%- gettext("The number of assignments of this type that will be dropped. The lowest scoring assignments are dropped first.") %> + <% if (model.get('type') !== '') { %> + <% if (assignmentList.length !== model.get('min_count')){ %> +
+
+ + <%- gettext("Warning: ") %> + <%- + edx.StringUtils.interpolate( + gettext("The number of {type} assignments defined here does not match the current number of {type} assignments in the course:"), + {type: model.get('type')}, + ) + %> +
+
+ <% if (assignmentList.length == 0){ %> +
<%- gettext("There are no assignments of this type in the course.") %>
+ <% } else { %> + <%- + edx.StringUtils.interpolate( + gettext("{assignment_count} {type} assignment(s) found:"), + { + assignment_count: assignmentList.length, + type: model.get('type') + }, + ) + %> +
    + <% _.each(assignmentList, function (qualifiedSubsectionName){ %> +
  1. <%- qualifiedSubsectionName %>
  2. + <% }) %> +
+ <% } %> +
+
+ <% } else { %> +
+ + <%- + edx.StringUtils.interpolate( + gettext("The number of {type} assignments in the course matches the number defined here."), + {type: model.get('type')}, + ) + %> +
+ <% } %> + <% } %> +
<%- gettext("Delete") %>
diff --git a/cms/templates/settings_graders.html b/cms/templates/settings_graders.html index bde135122155..a923c4875df7 100644 --- a/cms/templates/settings_graders.html +++ b/cms/templates/settings_graders.html @@ -31,9 +31,12 @@ <%block name="requirejs"> require(["js/factories/settings_graders"], function(SettingsGradersFactory) { SettingsGradersFactory( - _.extend(${dump_js_escaped_json(course_details, cls=CourseSettingsEncoder) | n, decode.utf8}, - {is_credit_course: ${is_credit_course | n, dump_js_escaped_json}}), - "${grading_url | n, js_escaped_string}" + _.extend( + ${dump_js_escaped_json(course_details, cls=CourseSettingsEncoder) | n, decode.utf8}, + { is_credit_course: ${is_credit_course | n, dump_js_escaped_json} } + ), + "${grading_url | n, js_escaped_string}", + ${course_assignment_lists | n, dump_js_escaped_json}, ); });