feat: add assignment type count warning (#32068)

* feat: add assignment type count warning

* style: quality
This commit is contained in:
Jansen Kantor
2023-04-14 12:26:29 -04:00
committed by GitHub
parent e973266b2f
commit de047cd6f9
8 changed files with 137 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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