Commit 39024a7f by Don Mitchell

Grading mostly working

parent 50d7e616
...@@ -7,7 +7,7 @@ from django.test.client import Client ...@@ -7,7 +7,7 @@ from django.test.client import Client
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from xmodule.modulestore import Location from xmodule.modulestore import Location
from cms.djangoapps.models.settings.course_details import CourseDetails,\ from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseDetailsEncoder CourseSettingsEncoder
import json import json
from common.djangoapps.util import converters from common.djangoapps.util import converters
...@@ -87,7 +87,7 @@ class CourseDetailsTestCase(TestCase): ...@@ -87,7 +87,7 @@ class CourseDetailsTestCase(TestCase):
def test_encoder(self): def test_encoder(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
jsondetails = json.dumps(details, cls=CourseDetailsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
jsondetails = json.loads(jsondetails) jsondetails = json.loads(jsondetails)
self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=") self.assertTupleEqual(Location(jsondetails['course_location']), self.course_location, "Location !=")
# Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense. # Note, start_date is being initialized someplace. I'm not sure why b/c the default will make no sense.
...@@ -164,23 +164,22 @@ class CourseDetailsViewTest(TestCase): ...@@ -164,23 +164,22 @@ class CourseDetailsViewTest(TestCase):
def alter_field(self, url, details, field, val): def alter_field(self, url, details, field, val):
details[field] = val details[field] = val
jsondetails = json.dumps(details, cls=CourseDetailsEncoder) jsondetails = json.dumps(details, cls=CourseSettingsEncoder)
resp = self.client.post(url, jsondetails) resp = self.client.post(url, jsondetails)
self.assertDictEqual(json.loads(resp), details, field + val) self.assertDictEqual(json.loads(resp.content), details.__dict__, field + val)
def test_update_and_fetch(self): def test_update_and_fetch(self):
details = CourseDetails.fetch(self.course_location) details = CourseDetails.fetch(self.course_location)
details_loc = self.course_location.dict().copy()
details_loc['section'] = 'details'
resp = self.client.get(reverse('contentstore.views.get_course_settings', kwargs=self.course_location.dict())) resp = self.client.get(reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
'name' : self.course_location.name }))
self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True) self.assertContains(resp, '<li><a href="#" class="is-shown" data-section="details">Course Details</a></li>', status_code=200, html=True)
# resp s/b json from here on # resp s/b json from here on
url = reverse('contentstore.views.course_settings_updates', kwargs=details_loc) url = reverse('course_settings', kwargs={'org' : self.course_location.org, 'course' : self.course_location.course,
'name' : self.course_location.name, 'section' : 'details' })
resp = self.client.get(url) resp = self.client.get(url)
jsondetails = json.dumps(details, cls=CourseDetailsEncoder) self.assertDictEqual(json.loads(resp.content), details.__dict__, "virgin get")
self.assertDictEqual(resp, jsondetails, "virgin get")
self.alter_field(url, details, 'start_date', time.time() * 1000) self.alter_field(url, details, 'start_date', time.time() * 1000)
self.alter_field(url, details, 'start_date', time.time() * 1000 + 60 * 60 * 24) self.alter_field(url, details, 'start_date', time.time() * 1000 + 60 * 60 * 24)
......
...@@ -46,7 +46,8 @@ import time ...@@ -46,7 +46,8 @@ import time
from contentstore import course_info_model from contentstore import course_info_model
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from cms.djangoapps.models.settings.course_details import CourseDetails,\ from cms.djangoapps.models.settings.course_details import CourseDetails,\
CourseDetailsEncoder CourseSettingsEncoder
from cms.djangoapps.models.settings.course_grading import CourseGradingModel
# to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz' # to install PIL on MacOSX: 'easy_install http://dist.repoze.org/PIL-1.1.6.tar.gz'
...@@ -955,7 +956,7 @@ def get_course_settings(request, org, course, name): ...@@ -955,7 +956,7 @@ def get_course_settings(request, org, course, name):
return render_to_response('settings.html', { return render_to_response('settings.html', {
'active_tab': 'settings-tab', 'active_tab': 'settings-tab',
'context_course': course_module, 'context_course': course_module,
'course_details' : json.dumps(course_details, cls=CourseDetailsEncoder) 'course_details' : json.dumps(course_details, cls=CourseSettingsEncoder)
}) })
@expect_json @expect_json
...@@ -963,7 +964,7 @@ def get_course_settings(request, org, course, name): ...@@ -963,7 +964,7 @@ def get_course_settings(request, org, course, name):
@ensure_csrf_cookie @ensure_csrf_cookie
def course_settings_updates(request, org, course, name, section): def course_settings_updates(request, org, course, name, section):
""" """
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely restful CRUD operations on course settings. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page. through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit org, course: Attributes of the Location for the item to edit
...@@ -971,14 +972,42 @@ def course_settings_updates(request, org, course, name, section): ...@@ -971,14 +972,42 @@ def course_settings_updates(request, org, course, name, section):
""" """
if section == 'details': if section == 'details':
manager = CourseDetails manager = CourseDetails
elif section == 'grading':
manager = CourseGradingModel
else: return else: return
if request.method == 'GET': if request.method == 'GET':
# Cannot just do a get w/o knowing the course name :-( # Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseDetailsEncoder), return HttpResponse(json.dumps(manager.fetch(Location(['i4x', org, course, 'course',name])), cls=CourseSettingsEncoder),
mimetype="application/json")
elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseSettingsEncoder),
mimetype="application/json")
@expect_json
@login_required
@ensure_csrf_cookie
def course_grader_updates(request, org, course, name, grader_index=None):
"""
restful CRUD operations on course_info updates. This differs from get_course_settings by communicating purely
through json (not rendering any html) and handles section level operations rather than whole page.
org, course: Attributes of the Location for the item to edit
"""
if request.method == 'POST' and 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META:
real_method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE']
else:
real_method = request.method
if real_method == 'GET':
# Cannot just do a get w/o knowing the course name :-(
return HttpResponse(json.dumps(CourseGradingModel.fetch_grader(Location(['i4x', org, course, 'course',name]), grader_index)),
mimetype="application/json") mimetype="application/json")
elif real_method == "DELETE":
# ??? Shoudl this return anything? Perhaps success fail?
CourseGradingModel.delete_grader(Location(['i4x', org, course, 'course',name]), grader_index)
elif request.method == 'POST': # post or put, doesn't matter. elif request.method == 'POST': # post or put, doesn't matter.
return HttpResponse(json.dumps(manager.update_from_json(request.POST), cls=CourseDetailsEncoder), return HttpResponse(json.dumps(CourseGradingModel.update_grader_from_json(Location(['i4x', org, course, 'course',name]), request.POST)),
mimetype="application/json") mimetype="application/json")
......
...@@ -6,6 +6,7 @@ from json.encoder import JSONEncoder ...@@ -6,6 +6,7 @@ from json.encoder import JSONEncoder
import time import time
from contentstore.utils import get_modulestore from contentstore.utils import get_modulestore
from util.converters import jsdate_to_time, time_to_date from util.converters import jsdate_to_time, time_to_date
from cms.djangoapps.models.settings import course_grading
class CourseDetails: class CourseDetails:
def __init__(self, location): def __init__(self, location):
...@@ -131,10 +132,11 @@ class CourseDetails: ...@@ -131,10 +132,11 @@ class CourseDetails:
# Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm # Could just generate and return a course obj w/o doing any db reads, but I put the reads in as a means to confirm
# it persisted correctly # it persisted correctly
return CourseDetails.fetch(course_location) return CourseDetails.fetch(course_location)
class CourseDetailsEncoder(json.JSONEncoder): # TODO move to a more general util? Is there a better way to do the isinstance model check?
class CourseSettingsEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
if isinstance(obj, CourseDetails): if isinstance(obj, CourseDetails) or isinstance(obj, course_grading.CourseGradingModel):
return obj.__dict__ return obj.__dict__
elif isinstance(obj, Location): elif isinstance(obj, Location):
return obj.dict() return obj.dict()
......
from xmodule.modulestore import Location
from contentstore.utils import get_modulestore
import datetime
import re
from common.djangoapps.util import converters
import time
class CourseGradingModel:
"""
Basically a DAO and Model combo for CRUD operations pertaining to grading policy.
"""
def __init__(self, course_descriptor):
self.course_location = course_descriptor.location
self.graders = [CourseGradingModel.jsonize_grader(i, grader) for i, grader in enumerate(course_descriptor.raw_grader)] # weights transformed to ints [0..100]
self.grade_cutoffs = course_descriptor.grade_cutoffs
self.grace_period = CourseGradingModel.convert_set_grace_period(course_descriptor)
@classmethod
def fetch(cls, course_location):
"""
Fetch the course details for the given course from persistence and return a CourseDetails model.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
model = cls(descriptor)
return model
@staticmethod
def fetch_grader(course_location, index):
"""
Fetch the course's nth grader
Returns an empty dict if there's no such grader.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
# # but that would require not using CourseDescriptor's field directly. Opinions?
# FIXME how do I tell it to ignore index? Is there another iteration mech I should use?
if len(descriptor.raw_grader) > index:
return CourseGradingModel.jsonize_grader(index, descriptor.raw_grader[index])
# return empty model
else:
return {
"id" : index,
"type" : "",
"min_count" : 0,
"drop_count" : 0,
"short_label" : None,
"weight" : 0
}
@staticmethod
def fetch_cutoffs(course_location):
"""
Fetch the course's grade cutoffs.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
return descriptor.grade_cutoffs
@staticmethod
def fetch_grace_period(course_location):
"""
Fetch the course's default grace period.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
return {'grace_period' : CourseGradingModel.convert_set_grace_period(descriptor) }
@staticmethod
def update_from_json(jsondict):
"""
Decode the json into CourseGradingModel and save any changes. Returns the modified model.
Probably not the usual path for updates as it's too coarse grained.
"""
course_location = jsondict['course_location']
descriptor = get_modulestore(course_location).get_item(course_location)
graders_parsed = [CourseGradingModel.parse_grader(jsonele) for jsonele in jsondict['graders']]
descriptor.raw_grader = graders_parsed
descriptor.grade_cutoffs = jsondict['grade_cutoffs']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
CourseGradingModel.update_grace_period_from_json(course_location, jsondict['grace_period'])
return CourseGradingModel.fetch(course_location)
@staticmethod
def update_grader_from_json(course_location, grader):
"""
Create or update the grader of the given type (string key) for the given course. Returns the modified
grader which is a full model on the client but not on the server (just a dict)
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
# # ??? it would be good if these had the course_location in them so that they stand alone sufficiently
# # but that would require not using CourseDescriptor's field directly. Opinions?
# parse removes the id; so, grab it before parse
index = grader.get('id', None)
grader = CourseGradingModel.parse_grader(grader)
if index < len(descriptor.raw_grader):
descriptor.raw_grader[index] = grader
else:
descriptor.raw_grader.append(grader)
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
return grader
@staticmethod
def update_cutoffs_from_json(course_location, cutoffs):
"""
Create or update the grade cutoffs for the given course. Returns sent in cutoffs (ie., no extra
db fetch).
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = cutoffs
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
return cutoffs
@staticmethod
def update_grace_period_from_json(course_location, graceperiodjson):
"""
Update the course's default grace period.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
if not isinstance(graceperiodjson, dict):
graceperiodjson = {'grace_period' : graceperiodjson}
grace_time = converters.jsdate_to_time(graceperiodjson['grace_period'])
# NOTE: this does not handle > 24 hours
grace_rep = time.strftime("%H hours %M minutes %S seconds", grace_time)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.metadata['graceperiod'] = grace_rep
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
return graceperiodjson
@staticmethod
def delete_grader(course_location, index):
"""
Delete the grader of the given type from the given course.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
if index < len(descriptor.raw_grader):
del descriptor.raw_grader[index]
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
# NOTE cannot delete cutoffs. May be useful to reset
@staticmethod
def delete_cutoffs(course_location, cutoffs):
"""
Resets the cutoffs to the defaults
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
descriptor.grade_cutoffs = descriptor.defaut_grading_policy['GRADE_CUTOFFS']
get_modulestore(course_location).update_item(course_location, descriptor.definition['data'])
return descriptor.grade_cutoffs
@staticmethod
def delete_grace_period(course_location):
"""
Delete the course's default grace period.
"""
if not isinstance(course_location, Location):
course_location = Location(course_location)
descriptor = get_modulestore(course_location).get_item(course_location)
del descriptor.metadata['graceperiod']
get_modulestore(course_location).update_metadata(course_location, descriptor.metadata)
@staticmethod
def convert_set_grace_period(descriptor):
# 5 hours 59 minutes 59 seconds => converted to iso format
rawgrace = descriptor.metadata.get('graceperiod', None)
if rawgrace:
parsedgrace = {str(key): val for (val, key) in re.findall('\s*(\d*)\s*(\w*)', rawgrace)}
gracedate = datetime.datetime.today()
gracedate = gracedate.replace(minute = int(parsedgrace.get('minutes',0)), hour = int(parsedgrace.get('hours',0)))
return gracedate.isoformat() + 'Z'
else: return None
@staticmethod
def parse_grader(json_grader):
# manual to clear out kruft
result = {
"type" : json_grader["type"],
"min_count" : json_grader.get('min_count', 0),
"drop_count" : json_grader.get('drop_count', 0),
"short_label" : json_grader.get('short_label', None),
"weight" : json_grader.get('weight', 0) / 100.0
}
return result
@staticmethod
def jsonize_grader(i, grader):
grader['id'] = i
if grader['weight']:
grader['weight'] *= 100
if not 'short_label' in grader:
grader['short_label'] = ""
return grader
\ No newline at end of file
<li class="input input-existing multi course-grading-assignment-list-item">
<div class="row row-col2">
<label for="course-grading-assignment-name">Assignment Type Name:</label>
<div class="field">
<div class="input course-grading-assignment-name">
<input type="text" class="long"
id="course-grading-assignment-name" value="<%= model.get('type') %>">
<span class="tip tip-stacked">e.g. Homework, Labs, Midterm Exams, Final Exam</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-shortname">Abbreviation:</label>
<div class="field">
<div class="input course-grading-shortname">
<input type="text" class="short"
id="course-grading-assignment-shortname"
value="<%= model.get('short_label') %>">
<span class="tip tip-inline">e.g. HW, Midterm, Final</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-gradeweight">Weight of Total
Grade:</label>
<div class="field">
<div class="input course-grading-gradeweight">
<input type="text" class="short"
id="course-grading-assignment-gradeweight"
value = "<%= model.get('weight') %>">
<span class="tip tip-inline">e.g. 25%</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-totalassignments">Total
Number:</label>
<div class="field">
<div class="input course-grading-totalassignments">
<input type="text" class="short"
id="course-grading-assignment-totalassignments"
value = "<%= model.get('min_count') %>">
<span class="tip tip-inline">total exercises assigned</span>
</div>
</div>
</div>
<div class="row row-col2">
<label for="course-grading-assignment-droppable">Number of
Droppable:</label>
<div class="field">
<div class="input course-grading-droppable">
<input type="text" class="short"
id="course-grading-assignment-droppable"
value = "<%= model.get('drop_count') %>">
<span class="tip tip-inline">total exercises that won't be graded</span>
</div>
</div>
</div> <a href="#" class="remove-item remove-grading-data"><span
class="delete-icon"></span> Delete Assignment Type</a>
</li>
...@@ -67,7 +67,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({ ...@@ -67,7 +67,7 @@ CMS.Models.Settings.CourseDetails = Backbone.Model.extend({
// NOTE don't return empty errors as that will be interpreted as an error state // NOTE don't return empty errors as that will be interpreted as an error state
}, },
urlRoot: function() { url: function() {
var location = this.get('location'); var location = this.get('location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details'; return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/details';
}, },
......
if (!CMS.Models['Settings']) CMS.Models.Settings = new Object();
CMS.Models.Settings.CourseGradingPolicy = Backbone.Model.extend({
defaults : {
course_location : null,
graders : null, // CourseGraderCollection
grade_cutoffs : null, // CourseGradeCutoff model
grace_period : null // either null or seconds of grace period
},
parse: function(attributes) {
if (attributes['course_location']) {
attributes.course_location = new CMS.Models.Location(attributes.course_location, {parse:true});
}
if (attributes['grace_period']) {
attributes.grace_period = new Date(attributes.grace_period);
}
if (attributes['graders']) {
var graderCollection;
if (this.has('graders')) {
graderCollection = this.get('graders');
graderCollection.reset(attributes.graders);
}
else {
graderCollection = new CMS.Models.Settings.CourseGraderCollection(attributes.graders);
graderCollection.course_location = attributes['course_location'] || this.get('course_location');
}
attributes.graders = graderCollection;
}
return attributes;
},
url : function() {
var location = this.get('course_location');
return '/' + location.get('org') + "/" + location.get('course') + '/settings/' + location.get('name') + '/section/grading';
}
});
CMS.Models.Settings.CourseGrader = Backbone.Model.extend({
defaults: {
"type" : "", // must be unique w/in collection (ie. w/in course)
"min_count" : 0,
"drop_count" : 0,
"short_label" : "", // what to use in place of type if space is an issue
"weight" : 0 // int 0..100
},
initialize: function() {
if (!this.collection)
console.log("damn");
},
parse : function(attrs) {
if (attrs['weight']) {
if (!_.isNumber(attrs.weight)) attrs.weight = parseInt(attrs.weight);
}
if (attrs['min_count']) {
if (!_.isNumber(attrs.min_count)) attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (!_.isNumber(attrs.drop_count)) attrs.drop_count = parseInt(attrs.drop_count);
}
return attrs;
},
validate : function(attrs) {
var errors = {};
if (attrs['type']) {
if (_.isEmpty(attrs['type'])) {
errors.type = "The assignment type must have a name.";
}
else {
// FIXME somehow this.collection is unbound sometimes. I can't track down when
var existing = this.collection && this.collection.some(function(other) { return (other != this) && (other.get('type') == attrs['type']);}, this);
if (existing) {
errors.type = "There's already another assignment type with this name.";
}
}
}
if (attrs['weight']) {
if (!parseInt(attrs.weight)) {
errors.weight = "Please enter an integer between 0 and 100.";
}
else {
attrs.weight = parseInt(attrs.weight); // see if this ensures value saved is int
if (this.collection && attrs.weight > 0) {
// if get() doesn't get the value before the call, use previous()
if ((this.collection.sumWeights() + attrs.weight - this.get('weight')) > 100)
errors.weight = "The weights cannot add to more than 100.";
}
}}
if (attrs['min_count']) {
if (!parseInt(attrs.min_count)) {
errors.min_count = "Please enter an integer.";
}
else attrs.min_count = parseInt(attrs.min_count);
}
if (attrs['drop_count']) {
if (!parseInt(attrs.drop_count)) {
errors.drop_count = "Please enter an integer.";
}
else attrs.drop_count = parseInt(attrs.drop_count);
}
if (attrs['min_count'] && attrs['drop_count'] && attrs.drop_count > attrs.min_count) {
errors.drop_count = "Cannot drop more " + attrs.type + " than will assigned.";
}
if (!_.isEmpty(errors)) return errors;
}
});
CMS.Models.Settings.CourseGraderCollection = Backbone.Collection.extend({
model : CMS.Models.Settings.CourseGrader,
course_location : null, // must be set to a Location object
url : function() {
return '/' + this.course_location.get('org') + "/" + this.course_location.get('course') + '/grades/' + this.course_location.get('name');
},
sumWeights : function() {
return this.reduce(function(subtotal, grader) { return subtotal + grader.get('weight'); }, 0);
}
});
\ No newline at end of file
...@@ -13,15 +13,31 @@ CMS.Models.Settings.CourseSettings = Backbone.Model.extend({ ...@@ -13,15 +13,31 @@ CMS.Models.Settings.CourseSettings = Backbone.Model.extend({
retrieve: function(submodel, callback) { retrieve: function(submodel, callback) {
if (this.get(submodel)) callback(); if (this.get(submodel)) callback();
else switch (submodel) { else {
case 'details': var cachethis = this;
this.set('details', new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')})).fetch({ switch (submodel) {
success : callback case 'details':
}); var details = new CMS.Models.Settings.CourseDetails({location: this.get('courseLocation')});
break; details.fetch( {
success : function(model) {
cachethis.set('details', model);
callback(model);
}
});
break;
case 'grading':
var grading = new CMS.Models.Settings.CourseGradingPolicy({course_location: this.get('courseLocation')});
grading.fetch( {
success : function(model) {
cachethis.set('grading', model);
callback(model);
}
});
break;
default: default:
break; break;
}
} }
} }
}) })
\ No newline at end of file
...@@ -716,6 +716,10 @@ ...@@ -716,6 +716,10 @@
} }
} }
} }
.grade-specific-bar {
height: 50px;
}
.grades { .grades {
position: relative; position: relative;
......
...@@ -38,6 +38,7 @@ urlpatterns = ('', ...@@ -38,6 +38,7 @@ urlpatterns = ('',
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/course_info/updates/(?P<provided_id>.*)$', 'contentstore.views.course_info_updates', name='course_info'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)$', 'contentstore.views.get_course_settings', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'), url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/settings/(?P<name>[^/]+)/section/(?P<section>[^/]+).*$', 'contentstore.views.course_settings_updates', name='course_settings'),
url(r'^(?P<org>[^/]+)/(?P<course>[^/]+)/grades/(?P<name>[^/]+)/(?P<grader_index>.*)$', 'contentstore.views.course_grader_updates', name='course_settings'),
url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages', url(r'^pages/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.static_pages',
name='static_pages'), name='static_pages'),
url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'), url(r'^edit_static/(?P<org>[^/]+)/(?P<course>[^/]+)/course/(?P<coursename>[^/]+)$', 'contentstore.views.edit_static', name='edit_static'),
......
...@@ -6,6 +6,7 @@ def time_to_date(time_obj): ...@@ -6,6 +6,7 @@ def time_to_date(time_obj):
""" """
Convert a time.time_struct to a true universal time (can pass to js Date constructor) Convert a time.time_struct to a true universal time (can pass to js Date constructor)
""" """
# TODO change to using the isoformat() function on datetime. js date can parse those
return calendar.timegm(time_obj) * 1000 return calendar.timegm(time_obj) * 1000
def jsdate_to_time(field): def jsdate_to_time(field):
......
...@@ -10,6 +10,7 @@ import json ...@@ -10,6 +10,7 @@ import json
import logging import logging
import requests import requests
import time import time
import copy
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -99,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -99,19 +100,11 @@ class CourseDescriptor(SequenceDescriptor):
self.set_grading_policy(self.definition['data'].get('grading_policy', None)) self.set_grading_policy(self.definition['data'].get('grading_policy', None))
def defaut_grading_policy(self):
def set_grading_policy(self, course_policy):
if course_policy is None:
course_policy = {}
""" """
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is Return a dict which is a copy of the default grading policy
missing, it reverts to the default.
""" """
default = {"GRADER" : [
default_policy_string = """
{
"GRADER" : [
{ {
"type" : "Homework", "type" : "Homework",
"min_count" : 12, "min_count" : 12,
...@@ -127,33 +120,41 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -127,33 +120,41 @@ class CourseDescriptor(SequenceDescriptor):
"weight" : 0.15 "weight" : 0.15
}, },
{ {
"type" : "Midterm", "type" : "Midterm Exam",
"name" : "Midterm Exam",
"short_label" : "Midterm", "short_label" : "Midterm",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.3 "weight" : 0.3
}, },
{ {
"type" : "Final", "type" : "Final Exam",
"name" : "Final Exam",
"short_label" : "Final", "short_label" : "Final",
"min_count" : 1,
"drop_count" : 0,
"weight" : 0.4 "weight" : 0.4
} }
], ],
"GRADE_CUTOFFS" : { "GRADE_CUTOFFS" : {
"A" : 0.87, "Pass" : 0.5
"B" : 0.7, }}
"C" : 0.6 return copy.deepcopy(default)
}
} def set_grading_policy(self, course_policy):
""" """
The JSON object can have the keys GRADER and GRADE_CUTOFFS. If either is
missing, it reverts to the default.
"""
if course_policy is None:
course_policy = {}
# Load the global settings as a dictionary # Load the global settings as a dictionary
grading_policy = json.loads(default_policy_string) grading_policy = self.defaut_grading_policy()
# Override any global settings with the course settings # Override any global settings with the course settings
grading_policy.update(course_policy) grading_policy.update(course_policy)
# Here is where we should parse any configurations, so that we can fail early # Here is where we should parse any configurations, so that we can fail early
grading_policy['RAW_GRADER'] = grading_policy['GRADER'] # used for cms access
grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER']) grading_policy['GRADER'] = grader_from_conf(grading_policy['GRADER'])
self._grading_policy = grading_policy self._grading_policy = grading_policy
...@@ -272,10 +273,26 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -272,10 +273,26 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def grader(self): def grader(self):
return self._grading_policy['GRADER'] return self._grading_policy['GRADER']
@property
def raw_grader(self):
return self._grading_policy['RAW_GRADER']
@raw_grader.setter
def raw_grader(self, value):
# NOTE WELL: this change will not update the processed graders. If we need that, this needs to call grader_from_conf
self._grading_policy['RAW_GRADER'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADER'] = value
@property @property
def grade_cutoffs(self): def grade_cutoffs(self):
return self._grading_policy['GRADE_CUTOFFS'] return self._grading_policy['GRADE_CUTOFFS']
@grade_cutoffs.setter
def grade_cutoffs(self, value):
self._grading_policy['GRADE_CUTOFFS'] = value
self.definition['data'].setdefault('grading_policy',{})['GRADE_CUTOFFS'] = value
@property @property
def tabs(self): def tabs(self):
......
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