Commit 3204c8b3 by zubair-arbi

show credit eligibility requirements in studio

ECOM-1591
parent 233aba74
...@@ -24,6 +24,8 @@ from xmodule.modulestore.django import modulestore ...@@ -24,6 +24,8 @@ from xmodule.modulestore.django import modulestore
from xmodule.contentstore.content import StaticContent from xmodule.contentstore.content import StaticContent
from xmodule.tabs import CourseTab from xmodule.tabs import CourseTab
from openedx.core.lib.course_tabs import CourseTabPluginManager 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 import EdxJSONEncoder
from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError from xmodule.modulestore.exceptions import ItemNotFoundError, DuplicateCourseError
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
...@@ -842,6 +844,7 @@ def settings_handler(request, course_key_string): ...@@ -842,6 +844,7 @@ def settings_handler(request, course_key_string):
""" """
course_key = CourseKey.from_string(course_key_string) course_key = CourseKey.from_string(course_key_string)
prerequisite_course_enabled = settings.FEATURES.get('ENABLE_PREREQUISITE_COURSES', False) 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): with modulestore().bulk_operations(course_key):
course_module = get_course_and_check_access(course_key, request.user) course_module = get_course_and_check_access(course_key, request.user)
if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET': if 'text/html' in request.META.get('HTTP_ACCEPT', '') and request.method == 'GET':
...@@ -867,6 +870,9 @@ def settings_handler(request, course_key_string): ...@@ -867,6 +870,9 @@ def settings_handler(request, course_key_string):
'upload_asset_url': upload_asset_url, 'upload_asset_url': upload_asset_url,
'course_handler_url': reverse_course_url('course_handler', course_key), 'course_handler_url': reverse_course_url('course_handler', course_key),
'language_options': settings.ALL_LANGUAGES, 'language_options': settings.ALL_LANGUAGES,
'credit_eligibility_enabled': credit_eligibility_enabled,
'is_credit_course': False,
'show_min_grade_warning': False,
} }
if prerequisite_course_enabled: if prerequisite_course_enabled:
courses, in_process_course_actions = get_courses_accessible_to_user(request) courses, in_process_course_actions = get_courses_accessible_to_user(request)
...@@ -876,6 +882,27 @@ def settings_handler(request, course_key_string): ...@@ -876,6 +882,27 @@ def settings_handler(request, course_key_string):
courses = _remove_in_process_courses(courses, in_process_course_actions) courses = _remove_in_process_courses(courses, in_process_course_actions)
settings_context.update({'possible_pre_requisite_courses': courses}) 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) return render_to_response('settings.html', settings_context)
elif 'application/json' in request.META.get('HTTP_ACCEPT', ''): elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET': if request.method == 'GET':
...@@ -961,6 +988,7 @@ def grading_handler(request, course_key_string, grader_index=None): ...@@ -961,6 +988,7 @@ def grading_handler(request, course_key_string, grader_index=None):
'course_locator': course_key, 'course_locator': course_key,
'course_details': json.dumps(course_details, cls=CourseSettingsEncoder), 'course_details': json.dumps(course_details, cls=CourseSettingsEncoder),
'grading_url': reverse_course_url('grading_handler', course_key), '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', ''): elif 'application/json' in request.META.get('HTTP_ACCEPT', ''):
if request.method == 'GET': if request.method == 'GET':
...@@ -973,6 +1001,11 @@ def grading_handler(request, course_key_string, grader_index=None): ...@@ -973,6 +1001,11 @@ def grading_handler(request, course_key_string, grader_index=None):
else: else:
return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index)) return JsonResponse(CourseGradingModel.fetch_grader(course_key, grader_index))
elif request.method in ('POST', 'PUT'): # post or put, doesn't matter. 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 # None implies update the whole model (cutoffs, graceperiod, and graders) not a specific grader
if grader_index is None: if grader_index is None:
return JsonResponse( return JsonResponse(
......
"""
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): ...@@ -14,6 +14,7 @@ class CourseGradingModel(object):
] # weights transformed to ints [0..100] ] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor) self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
self.minimum_grade_credit = course_descriptor.minimum_grade_credit
@classmethod @classmethod
def fetch(cls, course_key): def fetch(cls, course_key):
...@@ -62,6 +63,8 @@ class CourseGradingModel(object): ...@@ -62,6 +63,8 @@ class CourseGradingModel(object):
CourseGradingModel.update_grace_period_from_json(course_key, jsondict['grace_period'], user) 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) return CourseGradingModel.fetch(course_key)
@staticmethod @staticmethod
...@@ -119,6 +122,25 @@ class CourseGradingModel(object): ...@@ -119,6 +122,25 @@ class CourseGradingModel(object):
modulestore().update_item(descriptor, user.id) modulestore().update_item(descriptor, user.id)
@staticmethod @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): def delete_grader(course_key, index, user):
""" """
Delete the grader of the given type from the given course. Delete the grader of the given type from the given course.
......
...@@ -44,7 +44,8 @@ class CourseMetadata(object): ...@@ -44,7 +44,8 @@ class CourseMetadata(object):
'is_entrance_exam', 'is_entrance_exam',
'in_entrance_exam', 'in_entrance_exam',
'language', 'language',
'certificates' 'certificates',
'minimum_grade_credit',
] ]
@classmethod @classmethod
......
...@@ -173,6 +173,8 @@ FEATURES = { ...@@ -173,6 +173,8 @@ FEATURES = {
# How many seconds to show the bumper again, default is 7 days: # How many seconds to show the bumper again, default is 7 days:
'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600,
# Enable credit eligibility feature
'ENABLE_CREDIT_ELIGIBILITY': False,
} }
ENABLE_JASMINE = False ENABLE_JASMINE = False
......
...@@ -2,7 +2,7 @@ define([ ...@@ -2,7 +2,7 @@ define([
'jquery', 'js/models/settings/course_details', 'js/views/settings/main' 'jquery', 'js/models/settings/course_details', 'js/views/settings/main'
], function($, CourseDetailsModel, MainView) { ], function($, CourseDetailsModel, MainView) {
'use strict'; 'use strict';
return function (detailsUrl) { return function (detailsUrl, showMinGradeWarning) {
var model; var model;
// highlighting labels when fields are focused in // highlighting labels when fields are focused in
$('form :input') $('form :input')
...@@ -19,7 +19,8 @@ define([ ...@@ -19,7 +19,8 @@ define([
success: function(model) { success: function(model) {
var editor = new MainView({ var editor = new MainView({
el: $('.settings-details'), el: $('.settings-details'),
model: model model: model,
showMinGradeWarning: showMinGradeWarning
}); });
editor.render(); editor.render();
}, },
......
...@@ -5,7 +5,8 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -5,7 +5,8 @@ var CourseGradingPolicy = Backbone.Model.extend({
defaults : { defaults : {
graders : null, // CourseGraderCollection graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model 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) { parse: function(attributes) {
if (attributes['graders']) { if (attributes['graders']) {
...@@ -28,6 +29,11 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -28,6 +29,11 @@ var CourseGradingPolicy = Backbone.Model.extend({
minutes: 0 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; return attributes;
}, },
gracePeriodToDate : function() { gracePeriodToDate : function() {
...@@ -55,6 +61,13 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -55,6 +61,13 @@ var CourseGradingPolicy = Backbone.Model.extend({
minutes: parseInt(pieces[1], 10) 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) { validate : function(attrs) {
if(_.has(attrs, 'grace_period')) { if(_.has(attrs, 'grace_period')) {
if(attrs['grace_period'] === null) { if(attrs['grace_period'] === null) {
...@@ -63,6 +76,18 @@ var CourseGradingPolicy = Backbone.Model.extend({ ...@@ -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({ ...@@ -42,6 +42,7 @@ var GradingView = ValidatingView.extend({
this.clearValidationErrors(); this.clearValidationErrors();
this.renderGracePeriod(); this.renderGracePeriod();
this.renderMinimumGradeCredit();
// Create and render the grading type subs // Create and render the grading type subs
var self = this; var self = this;
...@@ -86,7 +87,8 @@ var GradingView = ValidatingView.extend({ ...@@ -86,7 +87,8 @@ var GradingView = ValidatingView.extend({
this.model.get('graders').push({}); this.model.get('graders').push({});
}, },
fieldToSelectorMap : { fieldToSelectorMap : {
'grace_period' : 'course-grading-graceperiod' 'grace_period' : 'course-grading-graceperiod',
'minimum_grade_credit' : 'course-minimum_grade_credit'
}, },
renderGracePeriod: function() { renderGracePeriod: function() {
var format = function(time) { var format = function(time) {
...@@ -97,11 +99,23 @@ var GradingView = ValidatingView.extend({ ...@@ -97,11 +99,23 @@ var GradingView = ValidatingView.extend({
format(grace_period.hours) + ':' + format(grace_period.minutes) 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) { setGracePeriod : function(event) {
this.clearValidationErrors(); this.clearValidationErrors();
var newVal = this.model.parseGracePeriod($(event.currentTarget).val()); var newVal = this.model.parseGracePeriod($(event.currentTarget).val());
this.model.set('grace_period', newVal, {validate: true}); 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) { updateModel : function(event) {
if (!this.selectorToField[event.currentTarget.id]) return; if (!this.selectorToField[event.currentTarget.id]) return;
...@@ -110,6 +124,10 @@ var GradingView = ValidatingView.extend({ ...@@ -110,6 +124,10 @@ var GradingView = ValidatingView.extend({
this.setGracePeriod(event); this.setGracePeriod(event);
break; break;
case 'minimum_grade_credit':
this.setMinimumGradeCredit(event);
break;
default: default:
this.setField(event); this.setField(event);
break; break;
......
define(["js/views/validation", "codemirror", "underscore", "jquery", "jquery.ui", "js/utils/date_utils", "js/models/uploads", 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, function(ValidatingView, CodeMirror, _, $, ui, DateUtils, FileUploadModel,
FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel) { FileUploadDialog, TriggerChangeEventOnEnter, LicenseView, LicenseModel, NotificationView) {
var DetailsView = ValidatingView.extend({ var DetailsView = ValidatingView.extend({
// Model class is CMS.Models.Settings.CourseDetails // Model class is CMS.Models.Settings.CourseDetails
...@@ -21,7 +22,8 @@ var DetailsView = ValidatingView.extend({ ...@@ -21,7 +22,8 @@ var DetailsView = ValidatingView.extend({
'click .action-upload-image': "uploadImage" '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>'); this.fileAnchorTemplate = _.template('<a href="<%= fullpath %>"> <i class="icon fa fa-file"></i><%= filename %></a>');
// fill in fields // fill in fields
this.$el.find("#course-language").val(this.model.get('language')); this.$el.find("#course-language").val(this.model.get('language'));
...@@ -49,6 +51,14 @@ var DetailsView = ValidatingView.extend({ ...@@ -49,6 +51,14 @@ var DetailsView = ValidatingView.extend({
showPreview: true showPreview: true
}); });
this.listenTo(this.licenseModel, 'change', this.handleLicenseChange); 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() { render: function() {
......
...@@ -326,6 +326,24 @@ ...@@ -326,6 +326,24 @@
width: flex-grid(5, 9); 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 // course link note
.note-promotion-courseURL { .note-promotion-courseURL {
box-shadow: 0 2px 1px $shadow-l1; box-shadow: 0 2px 1px $shadow-l1;
...@@ -731,6 +749,11 @@ ...@@ -731,6 +749,11 @@
#field-course-grading-graceperiod { #field-course-grading-graceperiod {
width: flex-grid(3, 9); width: flex-grid(3, 9);
} }
#field-course-minimum_grade_credit {
width: flex-grid(4, 9);
}
} }
&.assignment-types { &.assignment-types {
......
...@@ -5,9 +5,10 @@ ...@@ -5,9 +5,10 @@
<%namespace name='static' file='static_content.html'/> <%namespace name='static' file='static_content.html'/>
<%! <%!
import json
import urllib
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from contentstore import utils from contentstore import utils
import urllib
%> %>
<%block name="header_extras"> <%block name="header_extras">
...@@ -27,9 +28,10 @@ CMS.URL = CMS.URL || {}; ...@@ -27,9 +28,10 @@ CMS.URL = CMS.URL || {};
CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
</script> </script>
</%block> </%block>
<%block name="requirejs"> <%block name="requirejs">
require(["js/factories/settings"], function(SettingsFactory) { require(["js/factories/settings"], function(SettingsFactory) {
SettingsFactory("${details_url}"); SettingsFactory("${details_url}", ${json.dumps(show_min_grade_warning)});
}); });
</%block> </%block>
...@@ -116,8 +118,53 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}'; ...@@ -116,8 +118,53 @@ CMS.URL.UPLOAD_ASSET = '${upload_asset_url}';
</div> </div>
% endif % endif
</section> </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" /> <hr class="divide" />
% endif
<section class="group-settings schedule"> <section class="group-settings schedule">
<header> <header>
......
...@@ -73,8 +73,25 @@ ...@@ -73,8 +73,25 @@
</li> </li>
</ol> </ol>
</section> </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 &amp; 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" /> <hr class="divide" />
% endif
<section class="group-settings grade-rules"> <section class="group-settings grade-rules">
<header> <header>
...@@ -90,7 +107,6 @@ ...@@ -90,7 +107,6 @@
</li> </li>
</ol> </ol>
</section> </section>
<hr class="divide" /> <hr class="divide" />
<section class="group-settings assignment-types"> <section class="group-settings assignment-types">
......
...@@ -200,6 +200,5 @@ class AdvancedSettingsPage(CoursePage): ...@@ -200,6 +200,5 @@ class AdvancedSettingsPage(CoursePage):
'annotation_storage_url', 'annotation_storage_url',
'social_sharing_url', 'social_sharing_url',
'teams_configuration', 'teams_configuration',
'minimum_grade_credit',
'video_bumper', 'video_bumper',
] ]
""" Contains the APIs for course credit requirements """ """
Contains the APIs for course credit requirements.
"""
import logging import logging
import uuid import uuid
from django.db import transaction 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 ( from .exceptions import (
InvalidCreditRequirements, InvalidCreditRequirements,
InvalidCreditCourse, InvalidCreditCourse,
...@@ -122,6 +127,7 @@ def get_credit_requirements(course_key, namespace=None): ...@@ -122,6 +127,7 @@ def get_credit_requirements(course_key, namespace=None):
Returns: Returns:
Dict of requirements in the given namespace Dict of requirements in the given namespace
""" """
requirements = CreditRequirement.get_course_requirements(course_key, namespace) requirements = CreditRequirement.get_course_requirements(course_key, namespace)
...@@ -455,3 +461,21 @@ def _validate_requirements(requirements): ...@@ -455,3 +461,21 @@ def _validate_requirements(requirements):
) )
) )
return invalid_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= ...@@ -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 # Import here, because signal is registered at startup, but items in tasks
# are not yet able to be loaded # 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__) ...@@ -20,7 +20,7 @@ LOGGER = get_task_logger(__name__)
# pylint: disable=not-callable # pylint: disable=not-callable
@task(default_retry_delay=settings.CREDIT_TASK_DEFAULT_RETRY_DELAY, max_retries=settings.CREDIT_TASK_MAX_RETRIES) @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. """Updates course requirements table for a course.
Args: Args:
...@@ -39,7 +39,7 @@ def update_course_requirements(course_id): ...@@ -39,7 +39,7 @@ def update_course_requirements(course_id):
set_credit_requirements(course_key, requirements) set_credit_requirements(course_key, requirements)
except (InvalidKeyError, ItemNotFoundError, InvalidCreditRequirements) as exc: except (InvalidKeyError, ItemNotFoundError, InvalidCreditRequirements) as exc:
LOGGER.error('Error on adding the requirements for course %s - %s', course_id, unicode(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: else:
LOGGER.info('Requirements added for course %s', course_id) LOGGER.info('Requirements added for course %s', course_id)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment