show credit eligibility requirements in studio
ECOM-1591
This commit is contained in:
@@ -24,6 +24,8 @@ from xmodule.modulestore.django import modulestore
|
||||
from xmodule.contentstore.content import StaticContent
|
||||
from xmodule.tabs import CourseTab
|
||||
from openedx.core.lib.course_tabs import CourseTabPluginManager
|
||||
from openedx.core.djangoapps.credit.api import is_credit_course, get_credit_requirements
|
||||
from openedx.core.djangoapps.credit.tasks import update_credit_course_requirements
|
||||
from xmodule.modulestore import EdxJSONEncoder
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
|
||||
from opaque_keys import InvalidKeyError
|
||||
@@ -842,6 +844,7 @@ def settings_handler(request, course_key_string):
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_key_string)
|
||||
prerequisite_course_enabled = settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False)
|
||||
credit_eligibility_enabled = settings.FEATURES.get('ENABLE_CREDIT_ELIGIBILITY', False)
|
||||
with modulestore().bulk_operations(course_key):
|
||||
course_module = get_course_and_check_access(course_key, request.user)
|
||||
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
|
||||
@@ -867,6 +870,9 @@ def settings_handler(request, course_key_string):
|
||||
'upload_asset_url': upload_asset_url,
|
||||
'course_handler_url': reverse_course_url('course_handler', course_key),
|
||||
'language_options': settings.ALL_LANGUAGES,
|
||||
'credit_eligibility_enabled': credit_eligibility_enabled,
|
||||
'is_credit_course': False,
|
||||
'show_min_grade_warning': False,
|
||||
}
|
||||
if prerequisite_course_enabled:
|
||||
courses, in_process_course_actions = get_courses_accessible_to_user(request)
|
||||
@@ -876,6 +882,27 @@ def settings_handler(request, course_key_string):
|
||||
courses = _remove_in_process_courses(courses, in_process_course_actions)
|
||||
settings_context.update({'possible_pre_requisite_courses': courses})
|
||||
|
||||
if credit_eligibility_enabled:
|
||||
if is_credit_course(course_key):
|
||||
# get and all credit eligibility requirements
|
||||
credit_requirements = get_credit_requirements(course_key)
|
||||
# pair together requirements with same 'namespace' values
|
||||
paired_requirements = {}
|
||||
for requirement in credit_requirements:
|
||||
namespace = requirement.pop("namespace")
|
||||
paired_requirements.setdefault(namespace, []).append(requirement)
|
||||
|
||||
# if 'minimum_grade_credit' of a course is not set or 0 then
|
||||
# show warning message to course author.
|
||||
show_min_grade_warning = False if course_module.minimum_grade_credit > 0 else True
|
||||
settings_context.update(
|
||||
{
|
||||
'is_credit_course': True,
|
||||
'credit_requirements': paired_requirements,
|
||||
'show_min_grade_warning': show_min_grade_warning,
|
||||
}
|
||||
)
|
||||
|
||||
return render_to_response('settings.html', settings_context)
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
@@ -961,6 +988,7 @@ def grading_handler(request, course_key_string, grader_index=None):
|
||||
'course_locator': course_key,
|
||||
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
|
||||
'grading_url': reverse_course_url('grading_handler', course_key),
|
||||
'is_credit_course': is_credit_course(course_key),
|
||||
})
|
||||
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
|
||||
if request.method == 'GET':
|
||||
@@ -973,6 +1001,11 @@ def grading_handler(request, course_key_string, grader_index=None):
|
||||
else:
|
||||
return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index))
|
||||
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter.
|
||||
# update credit course requirements if 'minimum_grade_credit'
|
||||
# field value is changed
|
||||
if 'minimum_grade_credit' in request.json:
|
||||
update_credit_course_requirements.delay(unicode(course_key))
|
||||
|
||||
# None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
|
||||
if grader_index is None:
|
||||
return JsonResponse(
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Unit tests for credit eligibility UI in Studio.
|
||||
"""
|
||||
|
||||
import mock
|
||||
|
||||
from contentstore.tests.utils import CourseTestCase
|
||||
from contentstore.utils import reverse_course_url
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from openedx.core.djangoapps.credit.api import get_credit_requirements
|
||||
from openedx.core.djangoapps.credit.models import CreditCourse
|
||||
from openedx.core.djangoapps.credit.signals import listen_for_course_publish
|
||||
|
||||
|
||||
class CreditEligibilityTest(CourseTestCase):
|
||||
"""Base class to test the course settings details view in Studio for credit
|
||||
eligibility requirements.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(CreditEligibilityTest, self).setUp()
|
||||
self.course = CourseFactory.create(org='edX', number='dummy', display_name='Credit Course')
|
||||
self.course_details_url = reverse_course_url('settings_handler', unicode(self.course.id))
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': False})
|
||||
def test_course_details_with_disabled_setting(self):
|
||||
"""Test that user don't see credit eligibility requirements in response
|
||||
if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is not enabled.
|
||||
"""
|
||||
response = self.client.get_html(self.course_details_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, "Credit Eligibility Requirements")
|
||||
self.assertNotContains(response, "Steps needed for credit eligibility")
|
||||
|
||||
@mock.patch.dict("django.conf.settings.FEATURES", {'ENABLE_CREDIT_ELIGIBILITY': True})
|
||||
def test_course_details_with_enabled_setting(self):
|
||||
"""Test that credit eligibility requirements are present in
|
||||
response if the feature flag 'ENABLE_CREDIT_ELIGIBILITY' is enabled.
|
||||
"""
|
||||
# verify that credit eligibility requirements block don't show if the
|
||||
# course is not set as credit course
|
||||
response = self.client.get_html(self.course_details_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotContains(response, "Credit Eligibility Requirements")
|
||||
self.assertNotContains(response, "Steps needed for credit eligibility")
|
||||
|
||||
# verify that credit eligibility requirements block shows if the
|
||||
# course is set as credit course and it has eligibility requirements
|
||||
credit_course = CreditCourse(course_key=unicode(self.course.id), enabled=True)
|
||||
credit_course.save()
|
||||
self.assertEqual(len(get_credit_requirements(self.course.id)), 0)
|
||||
# test that after publishing course, minimum grade requirement is added
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
self.assertEqual(len(get_credit_requirements(self.course.id)), 1)
|
||||
|
||||
response = self.client.get_html(self.course_details_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Credit Eligibility Requirements")
|
||||
self.assertContains(response, "Steps needed for credit eligibility")
|
||||
@@ -14,6 +14,7 @@ class CourseGradingModel(object):
|
||||
] # weights transformed to ints [0..100]
|
||||
self.grade_cutoffs = course_descriptor.grade_cutoffs
|
||||
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
|
||||
self.minimum_grade_credit = course_descriptor.minimum_grade_credit
|
||||
|
||||
@classmethod
|
||||
def fetch(cls, course_key):
|
||||
@@ -62,6 +63,8 @@ class CourseGradingModel(object):
|
||||
|
||||
CourseGradingModel.update_grace_period_from_json(course_key, jsondict['grace_period'], user)
|
||||
|
||||
CourseGradingModel.update_minimum_grade_credit_from_json(course_key, jsondict['minimum_grade_credit'], user)
|
||||
|
||||
return CourseGradingModel.fetch(course_key)
|
||||
|
||||
@staticmethod
|
||||
@@ -118,6 +121,25 @@ class CourseGradingModel(object):
|
||||
|
||||
modulestore().update_item(descriptor, user.id)
|
||||
|
||||
@staticmethod
|
||||
def update_minimum_grade_credit_from_json(course_key, minimum_grade_credit, user):
|
||||
"""Update the course's default minimum grade requirement for credit.
|
||||
|
||||
Args:
|
||||
course_key(CourseKey): The course identifier
|
||||
minimum_grade_json(Float): Minimum grade value
|
||||
user(User): The user object
|
||||
|
||||
"""
|
||||
descriptor = modulestore().get_course(course_key)
|
||||
|
||||
# 'minimum_grade_credit' cannot be set to None
|
||||
if minimum_grade_credit is not None:
|
||||
minimum_grade_credit = minimum_grade_credit
|
||||
|
||||
descriptor.minimum_grade_credit = minimum_grade_credit
|
||||
modulestore().update_item(descriptor, user.id)
|
||||
|
||||
@staticmethod
|
||||
def delete_grader(course_key, index, user):
|
||||
"""
|
||||
|
||||
@@ -44,7 +44,8 @@ class CourseMetadata(object):
|
||||
'is_entrance_exam',
|
||||
'in_entrance_exam',
|
||||
'language',
|
||||
'certificates'
|
||||
'certificates',
|
||||
'minimum_grade_credit',
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -173,6 +173,8 @@ FEATURES = {
|
||||
# How many seconds to show the bumper again, default is 7 days:
|
||||
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
|
||||
|
||||
# Enable credit eligibility feature
|
||||
'ENABLE_CREDIT_ELIGIBILITY': False,
|
||||
}
|
||||
|
||||
ENABLE_JASMINE = False
|
||||
|
||||
@@ -2,7 +2,7 @@ define([
|
||||
'jquery', 'js/models/settings/course_details', 'js/views/settings/main'
|
||||
], function($, CourseDetailsModel, MainView) {
|
||||
'use strict';
|
||||
return function (detailsUrl) {
|
||||
return function (detailsUrl, showMinGradeWarning) {
|
||||
var model;
|
||||
// highlighting labels when fields are focused in
|
||||
$('form :input')
|
||||
@@ -19,7 +19,8 @@ define([
|
||||
success: function(model) {
|
||||
var editor = new MainView({
|
||||
el: $('.settings-details'),
|
||||
model: model
|
||||
model: model,
|
||||
showMinGradeWarning: showMinGradeWarning
|
||||
});
|
||||
editor.render();
|
||||
},
|
||||
|
||||
@@ -5,7 +5,8 @@ var CourseGradingPolicy = Backbone.Model.extend({
|
||||
defaults : {
|
||||
graders : null, // CourseGraderCollection
|
||||
grade_cutoffs : null, // CourseGradeCutoff model
|
||||
grace_period : null // either null or { hours: n, minutes: m, ...}
|
||||
grace_period : null, // either null or { hours: n, minutes: m, ...}
|
||||
minimum_grade_credit : null // either null or percentage
|
||||
},
|
||||
parse: function(attributes) {
|
||||
if (attributes['graders']) {
|
||||
@@ -28,6 +29,11 @@ var CourseGradingPolicy = Backbone.Model.extend({
|
||||
minutes: 0
|
||||
}
|
||||
}
|
||||
// If minimum_grade_credit is unset or equal to 0 on the server,
|
||||
// it's received as 0
|
||||
if (attributes.minimum_grade_credit === null) {
|
||||
attributes.minimum_grade_credit = 0;
|
||||
}
|
||||
return attributes;
|
||||
},
|
||||
gracePeriodToDate : function() {
|
||||
@@ -55,6 +61,13 @@ var CourseGradingPolicy = Backbone.Model.extend({
|
||||
minutes: parseInt(pieces[1], 10)
|
||||
}
|
||||
},
|
||||
parseMinimumGradeCredit : function(minimum_grade_credit) {
|
||||
// get the value of minimum grade credit value in percentage
|
||||
if (isNaN(minimum_grade_credit)) {
|
||||
return 0;
|
||||
}
|
||||
return parseInt(minimum_grade_credit);
|
||||
},
|
||||
validate : function(attrs) {
|
||||
if(_.has(attrs, 'grace_period')) {
|
||||
if(attrs['grace_period'] === null) {
|
||||
@@ -63,6 +76,18 @@ var CourseGradingPolicy = Backbone.Model.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
if(_.has(attrs, 'minimum_grade_credit')) {
|
||||
var minimum_grade_cutoff = _.values(attrs.grade_cutoffs).pop();
|
||||
if(isNaN(attrs.minimum_grade_credit) || attrs.minimum_grade_credit === null || attrs.minimum_grade_credit < minimum_grade_cutoff) {
|
||||
return {
|
||||
'minimum_grade_credit': interpolate(
|
||||
gettext('Not able to set passing grade to less than %(minimum_grade_cutoff)s%.'),
|
||||
{minimum_grade_cutoff: minimum_grade_cutoff * 100},
|
||||
true
|
||||
)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ var GradingView = ValidatingView.extend({
|
||||
this.clearValidationErrors();
|
||||
|
||||
this.renderGracePeriod();
|
||||
this.renderMinimumGradeCredit();
|
||||
|
||||
// Create and render the grading type subs
|
||||
var self = this;
|
||||
@@ -86,7 +87,8 @@ var GradingView = ValidatingView.extend({
|
||||
this.model.get('graders').push({});
|
||||
},
|
||||
fieldToSelectorMap : {
|
||||
'grace_period' : 'course-grading-graceperiod'
|
||||
'grace_period' : 'course-grading-graceperiod',
|
||||
'minimum_grade_credit' : 'course-minimum_grade_credit'
|
||||
},
|
||||
renderGracePeriod: function() {
|
||||
var format = function(time) {
|
||||
@@ -97,11 +99,23 @@ var GradingView = ValidatingView.extend({
|
||||
format(grace_period.hours) + ':' + format(grace_period.minutes)
|
||||
);
|
||||
},
|
||||
renderMinimumGradeCredit: function() {
|
||||
var minimum_grade_credit = this.model.get('minimum_grade_credit');
|
||||
this.$el.find('#course-minimum_grade_credit').val(
|
||||
parseFloat(minimum_grade_credit) * 100 + '%'
|
||||
);
|
||||
},
|
||||
setGracePeriod : function(event) {
|
||||
this.clearValidationErrors();
|
||||
var newVal = this.model.parseGracePeriod($(event.currentTarget).val());
|
||||
this.model.set('grace_period', newVal, {validate: true});
|
||||
},
|
||||
setMinimumGradeCredit : function(event) {
|
||||
this.clearValidationErrors();
|
||||
// get field value in float
|
||||
var newVal = this.model.parseMinimumGradeCredit($(event.currentTarget).val()) / 100;
|
||||
this.model.set('minimum_grade_credit', newVal, {validate: true});
|
||||
},
|
||||
updateModel : function(event) {
|
||||
if (!this.selectorToField[event.currentTarget.id]) return;
|
||||
|
||||
@@ -110,6 +124,10 @@ var GradingView = ValidatingView.extend({
|
||||
this.setGracePeriod(event);
|
||||
break;
|
||||
|
||||
case 'minimum_grade_credit':
|
||||
this.setMinimumGradeCredit(event);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.setField(event);
|
||||
break;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads",
|
||||
"js/views/uploads", "js/utils/change_on_enter", "js/views/license", "js/models/license", "jquery.timepicker", "date"],
|
||||
"js/views/uploads", "js/utils/change_on_enter", "js/views/license", "js/models/license",
|
||||
"js/views/feedback_notification", "jquery.timepicker", "date"],
|
||||
function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
|
||||
FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel) {
|
||||
FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel, NotificationView) {
|
||||
|
||||
var DetailsView = ValidatingView.extend({
|
||||
// Model class is CMS.Models.Settings.CourseDetails
|
||||
@@ -21,7 +22,8 @@ var DetailsView = ValidatingView.extend({
|
||||
'click .action-upload-image': "uploadImage"
|
||||
},
|
||||
|
||||
initialize : function() {
|
||||
initialize : function(options) {
|
||||
options = options || {};
|
||||
this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon fa fa-file"></i><%= filename %></a>');
|
||||
// fill in fields
|
||||
this.$el.find("#course-language").val(this.model.get('language'));
|
||||
@@ -49,6 +51,14 @@ var DetailsView = ValidatingView.extend({
|
||||
showPreview: true
|
||||
});
|
||||
this.listenTo(this.licenseModel, 'change', this.handleLicenseChange);
|
||||
|
||||
if (options.showMinGradeWarning || false) {
|
||||
new NotificationView.Warning({
|
||||
title: gettext("Credit Eligibility Requirements"),
|
||||
message: gettext("Minimum passing grade for credit is not set."),
|
||||
closeIcon: true
|
||||
}).show();
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
||||
@@ -326,6 +326,24 @@
|
||||
width: flex-grid(5, 9);
|
||||
}
|
||||
|
||||
// Credit eligibility requirements
|
||||
#credit-minimum-passing-grade {
|
||||
float: left;
|
||||
width: flex-grid(3, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
#credit-proctoring-requirements {
|
||||
float: left;
|
||||
width: flex-grid(3, 9);
|
||||
margin-right: flex-gutter();
|
||||
}
|
||||
|
||||
#credit-reverification-requirements {
|
||||
float: left;
|
||||
width: flex-grid(3, 9);
|
||||
}
|
||||
|
||||
// course link note
|
||||
.note-promotion-courseURL {
|
||||
box-shadow: 0 2px 1px $shadow-l1;
|
||||
@@ -731,6 +749,11 @@
|
||||
#field-course-grading-graceperiod {
|
||||
width: flex-grid(3, 9);
|
||||
}
|
||||
|
||||
#field-course-minimum_grade_credit {
|
||||
width: flex-grid(4, 9);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.assignment-types {
|
||||
@@ -985,4 +1008,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
|
||||
<%namespace name='static' file='static_content.html'/>
|
||||
<%!
|
||||
import json
|
||||
import urllib
|
||||
from django.utils.translation import ugettext as _
|
||||
from contentstore import utils
|
||||
import urllib
|
||||
%>
|
||||
|
||||
<%block name="header_extras">
|
||||
@@ -27,9 +28,10 @@ CMS.URL = CMS.URL || {};
|
||||
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
<%block name="requirejs">
|
||||
require(["js/factories/settings"], function(SettingsFactory) {
|
||||
SettingsFactory("${details_url}");
|
||||
SettingsFactory("${details_url}", ${json.dumps(show_min_grade_warning)});
|
||||
});
|
||||
</%block>
|
||||
|
||||
@@ -116,9 +118,54 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
% if credit_eligibility_enabled and is_credit_course:
|
||||
<section class="group-settings basic">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Credit Eligibility Requirements")}</h2>
|
||||
<span class="tip">${_("Steps needed for credit eligibility")}</span>
|
||||
</header>
|
||||
% if credit_requirements:
|
||||
<ol class="list-input">
|
||||
% if 'grade' in credit_requirements:
|
||||
<li class="field text is-not-editable" id="credit-minimum-passing-grade">
|
||||
<label for="minimum-passing-grade">${_("Minimum Passing Grade")}</label>
|
||||
% for requirement in credit_requirements['grade']:
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
|
||||
class="long" id="${requirement['name']}" value="${'{0:.0f}%'.format(float(requirement['criteria']['min_grade'] or 0)*100)}" readonly />
|
||||
% endfor
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if 'proctored_exam' in credit_requirements:
|
||||
<li class="field text is-not-editable" id="credit-proctoring-requirements">
|
||||
<label for="proctoring-requirements">${_("Successful Proctored Exam")}</label>
|
||||
% for requirement in credit_requirements['proctored_exam']:
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
|
||||
class="long" id="${requirement['name']}" value="${requirement['display_name']}" readonly />
|
||||
% endfor
|
||||
</li>
|
||||
% endif
|
||||
|
||||
% if 'reverification' in credit_requirements:
|
||||
<li class="field text is-not-editable" id="credit-reverification-requirements">
|
||||
<label for="reverification-requirements">${_("Successful In Course Reverification")}</label>
|
||||
% for requirement in credit_requirements['reverification']:
|
||||
## Translators: 'Access to Assessment 1' means the access for a requirement with name 'Assessment 1'
|
||||
<input title="${_('This field is disabled: this information cannot be changed.')}" type="text"
|
||||
class="long" id="${requirement['name']}" value="${_('Access to {display_name}').format(display_name=requirement['display_name'])}" readonly />
|
||||
% endfor
|
||||
</li>
|
||||
% endif
|
||||
</ol>
|
||||
% else:
|
||||
<p>No credit requirements found.</p>
|
||||
% endif
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
% endif
|
||||
|
||||
<section class="group-settings schedule">
|
||||
<header>
|
||||
<h2 class="title-2">${_('Course Schedule')}</h2>
|
||||
|
||||
@@ -73,9 +73,26 @@
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
% if settings.FEATURES.get("ENABLE_CREDIT_ELIGIBILITY", False) and is_credit_course:
|
||||
<section class="group-settings grade-rules">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Credit Grade & Eligibility")}</h2>
|
||||
<span class="tip">${_("Settings for credit eligibility")}</span>
|
||||
</header>
|
||||
|
||||
<ol class="list-input">
|
||||
<li class="field text" id="field-course-minimum_grade_credit">
|
||||
<label for="course-minimum_grade_credit">${_("Minimum Passing Grade to Earn Credit:")}</label>
|
||||
<input type="text" class="short time" id="course-minimum_grade_credit" value="0" placeholder="80%" autocomplete="off" />
|
||||
<span class="tip tip-inline">${_("Must be greater than or equal to passing grade")}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
<hr class="divide" />
|
||||
% endif
|
||||
|
||||
<section class="group-settings grade-rules">
|
||||
<header>
|
||||
<h2 class="title-2">${_("Grading Rules & Policies")}</h2>
|
||||
@@ -90,7 +107,6 @@
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<hr class="divide" />
|
||||
|
||||
<section class="group-settings assignment-types">
|
||||
|
||||
@@ -200,6 +200,5 @@ class AdvancedSettingsPage(CoursePage):
|
||||
'annotation_storage_url',
|
||||
'social_sharing_url',
|
||||
'teams_configuration',
|
||||
'minimum_grade_credit',
|
||||
'video_bumper',
|
||||
]
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
""" Contains the APIs for course credit requirements """
|
||||
"""
|
||||
Contains the APIs for course credit requirements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
from django.db import transaction
|
||||
|
||||
from student.models import User
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from student.models import User
|
||||
from .exceptions import (
|
||||
InvalidCreditRequirements,
|
||||
InvalidCreditCourse,
|
||||
@@ -96,32 +101,33 @@ def get_credit_requirements(course_key, namespace=None):
|
||||
|
||||
Example:
|
||||
>>> get_credit_requirements("course-v1-edX-DemoX-1T2015")
|
||||
{
|
||||
requirements =
|
||||
[
|
||||
{
|
||||
"namespace": "reverification",
|
||||
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
|
||||
"display_name": "Assessment 1",
|
||||
"criteria": {},
|
||||
},
|
||||
{
|
||||
"namespace": "proctored_exam",
|
||||
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
|
||||
"display_name": "Final Exam",
|
||||
"criteria": {},
|
||||
},
|
||||
{
|
||||
"namespace": "grade",
|
||||
"name": "grade",
|
||||
"display_name": "Grade",
|
||||
"criteria": {"min_grade": 0.8},
|
||||
},
|
||||
]
|
||||
}
|
||||
{
|
||||
requirements =
|
||||
[
|
||||
{
|
||||
"namespace": "reverification",
|
||||
"name": "i4x://edX/DemoX/edx-reverification-block/assessment_uuid",
|
||||
"display_name": "Assessment 1",
|
||||
"criteria": {},
|
||||
},
|
||||
{
|
||||
"namespace": "proctored_exam",
|
||||
"name": "i4x://edX/DemoX/proctoring-block/final_uuid",
|
||||
"display_name": "Final Exam",
|
||||
"criteria": {},
|
||||
},
|
||||
{
|
||||
"namespace": "grade",
|
||||
"name": "grade",
|
||||
"display_name": "Grade",
|
||||
"criteria": {"min_grade": 0.8},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
Returns:
|
||||
Dict of requirements in the given namespace
|
||||
|
||||
"""
|
||||
|
||||
requirements = CreditRequirement.get_course_requirements(course_key, namespace)
|
||||
@@ -455,3 +461,21 @@ def _validate_requirements(requirements):
|
||||
)
|
||||
)
|
||||
return invalid_requirements
|
||||
|
||||
|
||||
def is_credit_course(course_key):
|
||||
"""API method to check if course is credit or not.
|
||||
|
||||
Args:
|
||||
course_key(CourseKey): The course identifier string or CourseKey object
|
||||
|
||||
Returns:
|
||||
Bool True if the course is marked credit else False
|
||||
|
||||
"""
|
||||
try:
|
||||
course_key = CourseKey.from_string(unicode(course_key))
|
||||
except InvalidKeyError:
|
||||
return False
|
||||
|
||||
return CreditCourse.is_credit_course(course_key=course_key)
|
||||
|
||||
@@ -15,6 +15,6 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
|
||||
|
||||
# Import here, because signal is registered at startup, but items in tasks
|
||||
# are not yet able to be loaded
|
||||
from .tasks import update_course_requirements
|
||||
from .tasks import update_credit_course_requirements
|
||||
|
||||
update_course_requirements.delay(unicode(course_key))
|
||||
update_credit_course_requirements.delay(unicode(course_key))
|
||||
|
||||
@@ -20,7 +20,7 @@ LOGGER = get_task_logger(__name__)
|
||||
|
||||
# pylint: disable=not-callable
|
||||
@task(default_retry_delay=settings.CREDIT_TASK_DEFAULT_RETRY_DELAY, max_retries=settings.CREDIT_TASK_MAX_RETRIES)
|
||||
def update_course_requirements(course_id):
|
||||
def update_credit_course_requirements(course_id): # pylint: disable=invalid-name
|
||||
"""Updates course requirements table for a course.
|
||||
|
||||
Args:
|
||||
@@ -39,7 +39,7 @@ def update_course_requirements(course_id):
|
||||
set_credit_requirements(course_key, requirements)
|
||||
except (InvalidKeyError, ItemNotFoundError, InvalidCreditRequirements) as exc:
|
||||
LOGGER.error('Error on adding the requirements for course %s - %s', course_id, unicode(exc))
|
||||
raise update_course_requirements.retry(args=[course_id], exc=exc)
|
||||
raise update_credit_course_requirements.retry(args=[course_id], exc=exc)
|
||||
else:
|
||||
LOGGER.info('Requirements added for course %s', course_id)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user