feat: add assignment type count warning (#32068)
* feat: add assignment type count warning * style: quality
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -100,6 +100,7 @@ from ..utils import (
|
||||
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':
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -29,6 +29,53 @@
|
||||
<span class="tip tip-stacked"><%- gettext("The number of assignments of this type that will be dropped. The lowest scoring assignments are dropped first.") %></span>
|
||||
</div>
|
||||
|
||||
<% if (model.get('type') !== '') { %>
|
||||
<% if (assignmentList.length !== model.get('min_count')){ %>
|
||||
<div class="assignment-count-info assignment-count-warning">
|
||||
<div class="assignment-count-warning-header">
|
||||
<span class="icon fa fa-exclamation-circle" aria-hidden="true"></span>
|
||||
<span class="header-warning"><%- gettext("Warning: ") %></span>
|
||||
<%-
|
||||
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')},
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
<div class="assignment-count-warning-content">
|
||||
<% if (assignmentList.length == 0){ %>
|
||||
<div><%- gettext("There are no assignments of this type in the course.") %></div>
|
||||
<% } else { %>
|
||||
<%-
|
||||
edx.StringUtils.interpolate(
|
||||
gettext("{assignment_count} {type} assignment(s) found:"),
|
||||
{
|
||||
assignment_count: assignmentList.length,
|
||||
type: model.get('type')
|
||||
},
|
||||
)
|
||||
%>
|
||||
<ol class="assignment_type_count_list">
|
||||
<% _.each(assignmentList, function (qualifiedSubsectionName){ %>
|
||||
<li><%- qualifiedSubsectionName %></li>
|
||||
<% }) %>
|
||||
</ol>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<div class="assignment-count-info assignment-count-matches">
|
||||
<span class="icon fa fa-check-circle-o" aria-hidden="true"></span>
|
||||
<%-
|
||||
edx.StringUtils.interpolate(
|
||||
gettext("The number of {type} assignments in the course matches the number defined here."),
|
||||
{type: model.get('type')},
|
||||
)
|
||||
%>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
|
||||
<div class="actions">
|
||||
<a href="#" class="button delete-button standard remove-item remove-grading-data"><span class="delete-icon"></span><%- gettext("Delete") %></a>
|
||||
</div>
|
||||
|
||||
@@ -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},
|
||||
);
|
||||
});
|
||||
</%block>
|
||||
|
||||
Reference in New Issue
Block a user