Commit 43d4661c by Your Name

Merge branch 'master' into feature/kevin/groups_ui_changes

parents 3404011f 40d59faa
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
"/static/js/vendor/jquery.min.js", "/static/js/vendor/jquery.min.js",
"/static/js/vendor/json2.js", "/static/js/vendor/json2.js",
"/static/js/vendor/underscore-min.js", "/static/js/vendor/underscore-min.js",
"/static/js/vendor/backbone-min.js" "/static/js/vendor/backbone-min.js",
"/static/js/vendor/RequireJS.js"
] ]
} }
...@@ -87,7 +87,10 @@ $(document).ready(function() { ...@@ -87,7 +87,10 @@ $(document).ready(function() {
$('.unit').draggable({ $('.unit').draggable({
axis: 'y', axis: 'y',
handle: '.drag-handle', handle: '.drag-handle',
stack: '.unit', zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
...@@ -95,7 +98,10 @@ $(document).ready(function() { ...@@ -95,7 +98,10 @@ $(document).ready(function() {
$('.id-holder').draggable({ $('.id-holder').draggable({
axis: 'y', axis: 'y',
handle: '.section-item .drag-handle', handle: '.section-item .drag-handle',
stack: '.id-holder', zIndex: 999,
start: initiateHesitate,
drag: checkHoverState,
stop: removeHesitate,
revert: "invalid" revert: "invalid"
}); });
...@@ -179,10 +185,12 @@ function toggleSections(e) { ...@@ -179,10 +185,12 @@ function toggleSections(e) {
if($button.hasClass('is-activated')) { if($button.hasClass('is-activated')) {
$section.addClass('collapsed'); $section.addClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('collapsed').addClass('expand'); // first child in order to avoid the icons on the subsection lists which are not in the first child
$section.find('header .expand-collapse-icon').removeClass('collapse').addClass('expand');
} else { } else {
$section.removeClass('collapsed'); $section.removeClass('collapsed');
$section.find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); // first child in order to avoid the icons on the subsection lists which are not in the first child
$section.find('header .expand-collapse-icon').removeClass('expand').addClass('collapse');
} }
} }
...@@ -271,9 +279,67 @@ function removePolicyMetadata(e) { ...@@ -271,9 +279,67 @@ function removePolicyMetadata(e) {
saveSubsection() saveSubsection()
} }
CMS.HesitateEvent.toggleXpandHesitation = null;
function initiateHesitate(event, ui) {
CMS.HesitateEvent.toggleXpandHesitation = new CMS.HesitateEvent(expandSection, 'dragLeave', true);
$('.collapsed').on('dragEnter', CMS.HesitateEvent.toggleXpandHesitation, CMS.HesitateEvent.toggleXpandHesitation.trigger);
$('.collapsed').each(function() {
this.proportions = {width : this.offsetWidth, height : this.offsetHeight };
// reset b/c these were holding values from aborts
this.isover = false;
});
}
function checkHoverState(event, ui) {
// copied from jquery.ui.droppable.js $.ui.ddmanager.drag & other ui.intersect
var draggable = $(this).data("ui-draggable"),
x1 = (draggable.positionAbs || draggable.position.absolute).left + (draggable.helperProportions.width / 2),
y1 = (draggable.positionAbs || draggable.position.absolute).top + (draggable.helperProportions.height / 2);
$('.collapsed').each(function() {
// don't expand the thing being carried
if (ui.helper.is(this)) {
return;
}
$.extend(this, {offset : $(this).offset()});
var droppable = this,
l = droppable.offset.left,
r = l + droppable.proportions.width,
t = droppable.offset.top,
b = t + droppable.proportions.height;
if (l === r) {
// probably wrong values b/c invisible at the time of caching
droppable.proportions = { width : droppable.offsetWidth, height : droppable.offsetHeight };
r = l + droppable.proportions.width;
b = t + droppable.proportions.height;
}
// equivalent to the intersects test
var intersects = (l < x1 && // Right Half
x1 < r && // Left Half
t < y1 && // Bottom Half
y1 < b ), // Top Half
c = !intersects && this.isover ? "isout" : (intersects && !this.isover ? "isover" : null);
if(!c) {
return;
}
this[c] = true;
this[c === "isout" ? "isover" : "isout"] = false;
$(this).trigger(c === "isover" ? "dragEnter" : "dragLeave");
});
}
function removeHesitate(event, ui) {
$('.collapsed').off('dragEnter', CMS.HesitateEvent.toggleXpandHesitation.trigger);
CMS.HesitateEvent.toggleXpandHesitation = null;
}
function expandSection(event) { function expandSection(event) {
$(event.delegateTarget).removeClass('collapsed'); $(event.delegateTarget).removeClass('collapsed', 400);
$(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); // don't descend to icon's on children (which aren't under first child) only to this element's icon
$(event.delegateTarget).children().first().find('.expand-collapse-icon').removeClass('expand', 400).addClass('collapse');
} }
function onUnitReordered(event, ui) { function onUnitReordered(event, ui) {
......
...@@ -18,33 +18,31 @@ CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) { ...@@ -18,33 +18,31 @@ CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) {
this.timeoutEventId = null; this.timeoutEventId = null;
this.originalEvent = null; this.originalEvent = null;
this.onlyOnce = (onlyOnce === true); this.onlyOnce = (onlyOnce === true);
} };
CMS.HesitateEvent.DURATION = 400; CMS.HesitateEvent.DURATION = 800;
CMS.HesitateEvent.prototype.trigger = function(event) { CMS.HesitateEvent.prototype.trigger = function(event) {
console.log('trigger'); if (event.data.timeoutEventId == null) {
if (this.timeoutEventId === null) { event.data.timeoutEventId = window.setTimeout(
this.timeoutEventId = window.setTimeout(this.fireEvent, CMS.HesitateEvent.DURATION); function() { event.data.fireEvent(event); },
this.originalEvent = event; CMS.HesitateEvent.DURATION);
// is it wrong to bind to the below v $(event.currentTarget)? event.data.originalEvent = event;
$(this.originalEvent.delegateTarget).on(this.cancelSelector, this.untrigger); $(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger);
} }
} };
CMS.HesitateEvent.prototype.fireEvent = function(event) { CMS.HesitateEvent.prototype.fireEvent = function(event) {
console.log('fire'); event.data.timeoutEventId = null;
this.timeoutEventId = null; $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
$(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger); if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger);
if (this.onlyOnce) $(this.originalEvent.delegateTarget).off(this.originalEvent.type, this.trigger); event.data.executeOnTimeOut(event.data.originalEvent);
this.executeOnTimeOut(this.originalEvent); };
}
CMS.HesitateEvent.prototype.untrigger = function(event) { CMS.HesitateEvent.prototype.untrigger = function(event) {
console.log('untrigger'); if (event.data.timeoutEventId) {
if (this.timeoutEventId) { window.clearTimeout(event.data.timeoutEventId);
window.clearTimeout(this.timeoutEventId); $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger);
$(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger);
} }
this.timeoutEventId = null; event.data.timeoutEventId = null;
} };
\ No newline at end of file \ No newline at end of file
from django.core.management.base import BaseCommand, CommandError
import os
from optparse import make_option
from student.models import UserProfile
import csv
class Command(BaseCommand):
help = """
Sets or gets certificate restrictions for users
from embargoed countries. (allow_certificate in
userprofile)
CSV should be comma delimited with double quoted entries.
$ ... cert_restriction --import path/to/userlist.csv
Export a list of students who have "allow_certificate" in
userprofile set to True
$ ... cert_restriction --output path/to/export.csv
Enable a single user so she is not on the restricted list
$ ... cert_restriction -e user
Disable a single user so she is on the restricted list
$ ... cert_restriction -d user
"""
option_list = BaseCommand.option_list + (
make_option('-i', '--import',
metavar='IMPORT_FILE',
dest='import',
default=False,
help='csv file to import, comma delimitted file with '
'double-quoted entries'),
make_option('-o', '--output',
metavar='EXPORT_FILE',
dest='output',
default=False,
help='csv file to export'),
make_option('-e', '--enable',
metavar='STUDENT',
dest='enable',
default=False,
help="enable a single student's certificate"),
make_option('-d', '--disable',
metavar='STUDENT',
dest='disable',
default=False,
help="disable a single student's certificate")
)
def handle(self, *args, **options):
if options['output']:
if os.path.exists(options['output']):
raise CommandError("File {0} already exists".format(
options['output']))
disabled_users = UserProfile.objects.filter(
allow_certificate=False)
with open(options['output'], 'w') as csvfile:
csvwriter = csv.writer(csvfile, delimiter=',', quotechar='"',
quoting=csv.QUOTE_MINIMAL)
for user in disabled_users:
csvwriter.writerow([user.user.username])
elif options['import']:
if not os.path.exists(options['import']):
raise CommandError("File {0} does not exist".format(
options['import']))
print "Importing students from {0}".format(options['import'])
students = None
with open(options['import']) as csvfile:
student_list = csv.reader(csvfile, delimiter=',',
quotechar='"')
students = [student[0] for student in student_list]
if not students:
raise CommandError(
"Unable to read student data from {0}".format(
options['import']))
UserProfile.objects.filter(user__username__in=students).update(
allow_certificate=False)
elif options['enable']:
print "Enabling {0} for certificate download".format(
options['enable'])
cert_allow = UserProfile.objects.get(
user__username=options['enable'])
cert_allow.allow_certificate = True
cert_allow.save()
elif options['disable']:
print "Disabling {0} for certificate download".format(
options['disable'])
cert_allow = UserProfile.objects.get(
user__username=options['disable'])
cert_allow.allow_certificate = False
cert_allow.save()
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'UserProfile.allow_certificate'
db.add_column('auth_userprofile', 'allow_certificate',
self.gf('django.db.models.fields.BooleanField')(default=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'UserProfile.allow_certificate'
db.delete_column('auth_userprofile', 'allow_certificate')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.courseenrollment': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.testcenterregistration': {
'Meta': {'object_name': 'TestCenterRegistration'},
'accommodation_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'accommodation_request': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '1024', 'blank': 'True'}),
'authorization_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'client_authorization_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'eligibility_appointment_date_first': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'eligibility_appointment_date_last': ('django.db.models.fields.DateField', [], {'db_index': 'True'}),
'exam_series_code': ('django.db.models.fields.CharField', [], {'max_length': '15', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'testcenter_user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['student.TestCenterUser']"}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.testcenteruser': {
'Meta': {'object_name': 'TestCenterUser'},
'address_1': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'address_2': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'address_3': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
'candidate_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'db_index': 'True'}),
'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'client_candidate_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}),
'company_name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '50', 'blank': 'True'}),
'confirmed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'country': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'db_index': 'True', 'blank': 'True'}),
'extension': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '8', 'blank': 'True'}),
'fax': ('django.db.models.fields.CharField', [], {'max_length': '35', 'blank': 'True'}),
'fax_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '50', 'db_index': 'True'}),
'middle_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'phone': ('django.db.models.fields.CharField', [], {'max_length': '35'}),
'phone_country_code': ('django.db.models.fields.CharField', [], {'max_length': '3', 'db_index': 'True'}),
'postal_code': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}),
'processed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'db_index': 'True'}),
'salutation': ('django.db.models.fields.CharField', [], {'max_length': '50', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'suffix': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'upload_error_message': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}),
'upload_status': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '20', 'blank': 'True'}),
'uploaded_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'default': 'None', 'to': "orm['auth.User']", 'unique': 'True'}),
'user_updated_at': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
\ No newline at end of file
...@@ -90,6 +90,7 @@ class UserProfile(models.Model): ...@@ -90,6 +90,7 @@ class UserProfile(models.Model):
) )
mailing_address = models.TextField(blank=True, null=True) mailing_address = models.TextField(blank=True, null=True)
goals = models.TextField(blank=True, null=True) goals = models.TextField(blank=True, null=True)
allow_certificate = models.BooleanField(default=1)
def get_meta(self): def get_meta(self):
js_str = self.meta js_str = self.meta
......
...@@ -135,7 +135,7 @@ def cert_info(user, course): ...@@ -135,7 +135,7 @@ def cert_info(user, course):
Get the certificate info needed to render the dashboard section for the given Get the certificate info needed to render the dashboard section for the given
student and course. Returns a dictionary with keys: student and course. Returns a dictionary with keys:
'status': one of 'generating', 'ready', 'notpassing', 'processing' 'status': one of 'generating', 'ready', 'notpassing', 'processing', 'restricted'
'show_download_url': bool 'show_download_url': bool
'download_url': url, only present if show_download_url is True 'download_url': url, only present if show_download_url is True
'show_disabled_download_button': bool -- true if state is 'generating' 'show_disabled_download_button': bool -- true if state is 'generating'
...@@ -168,6 +168,7 @@ def _cert_info(user, course, cert_status): ...@@ -168,6 +168,7 @@ def _cert_info(user, course, cert_status):
CertificateStatuses.regenerating: 'generating', CertificateStatuses.regenerating: 'generating',
CertificateStatuses.downloadable: 'ready', CertificateStatuses.downloadable: 'ready',
CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.notpassing: 'notpassing',
CertificateStatuses.restricted: 'restricted',
} }
status = template_state.get(cert_status['status'], default_status) status = template_state.get(cert_status['status'], default_status)
...@@ -176,7 +177,7 @@ def _cert_info(user, course, cert_status): ...@@ -176,7 +177,7 @@ def _cert_info(user, course, cert_status):
'show_download_url': status == 'ready', 'show_download_url': status == 'ready',
'show_disabled_download_button': status == 'generating',} 'show_disabled_download_button': status == 'generating',}
if (status in ('generating', 'ready', 'notpassing') and if (status in ('generating', 'ready', 'notpassing', 'restricted') and
course.end_of_course_survey_url is not None): course.end_of_course_survey_url is not None):
d.update({ d.update({
'show_survey_button': True, 'show_survey_button': True,
...@@ -192,7 +193,7 @@ def _cert_info(user, course, cert_status): ...@@ -192,7 +193,7 @@ def _cert_info(user, course, cert_status):
else: else:
d['download_url'] = cert_status['download_url'] d['download_url'] = cert_status['download_url']
if status in ('generating', 'ready', 'notpassing'): if status in ('generating', 'ready', 'notpassing', 'restricted'):
if 'grade' not in cert_status: if 'grade' not in cert_status:
# Note: as of 11/20/2012, we know there are students in this state-- cs169.1x, # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x,
# who need to be regraded (we weren't tracking 'notpassing' at first). # who need to be regraded (we weren't tracking 'notpassing' at first).
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
<script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/mathjax-MathJax-c9db6ac/MathJax.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/jquery.tinymce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script> <script type="text/javascript" src="<%= common_js_root %>/vendor/tiny_mce/tiny_mce.js"></script>
<script type="text/javascript" src="<%= common_js_root %>/vendor/RequireJS.js"></script>
<script type="text/javascript"> <script type="text/javascript">
AjaxPrefix.addAjaxPrefix(jQuery, function() { AjaxPrefix.addAjaxPrefix(jQuery, function() {
......
...@@ -21,6 +21,8 @@ from .xml_module import XmlDescriptor ...@@ -21,6 +21,8 @@ from .xml_module import XmlDescriptor
from xmodule.modulestore import Location from xmodule.modulestore import Location
import self_assessment_module import self_assessment_module
import open_ended_module import open_ended_module
from combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from .stringify import stringify_children
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -138,12 +140,19 @@ class CombinedOpenEndedModule(XModule): ...@@ -138,12 +140,19 @@ class CombinedOpenEndedModule(XModule):
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
self._max_score = int(self.metadata.get('max_score', MAX_SCORE)) self._max_score = int(self.metadata.get('max_score', MAX_SCORE))
rubric_renderer = CombinedOpenEndedRubric(system, True)
try:
rubric_feedback = rubric_renderer.render_rubric(stringify_children(definition['rubric']))
except RubricParsingError:
log.error("Failed to parse rubric in location: {1}".format(location))
raise
#Static data is passed to the child modules to render #Static data is passed to the child modules to render
self.static_data = { self.static_data = {
'max_score': self._max_score, 'max_score': self._max_score,
'max_attempts': self.max_attempts, 'max_attempts': self.max_attempts,
'prompt': definition['prompt'], 'prompt': definition['prompt'],
'rubric': definition['rubric'] 'rubric': definition['rubric'],
'display_name': self.display_name
} }
self.task_xml = definition['task_xml'] self.task_xml = definition['task_xml']
...@@ -295,6 +304,7 @@ class CombinedOpenEndedModule(XModule): ...@@ -295,6 +304,7 @@ class CombinedOpenEndedModule(XModule):
'task_count': len(self.task_xml), 'task_count': len(self.task_xml),
'task_number': self.current_task_number + 1, 'task_number': self.current_task_number + 1,
'status': self.get_status(), 'status': self.get_status(),
'display_name': self.display_name
} }
return context return context
......
...@@ -3,20 +3,37 @@ from lxml import etree ...@@ -3,20 +3,37 @@ from lxml import etree
log=logging.getLogger(__name__) log=logging.getLogger(__name__)
class RubricParsingError(Exception):
pass
class CombinedOpenEndedRubric(object): class CombinedOpenEndedRubric(object):
@staticmethod def __init__ (self, system, view_only = False):
def render_rubric(rubric_xml, system): self.has_score = False
self.view_only = view_only
self.system = system
def render_rubric(self, rubric_xml):
'''
render_rubric: takes in an xml string and outputs the corresponding
html for that xml, given the type of rubric we're generating
Input:
rubric_xml: an string that has not been parsed into xml that
represents this particular rubric
Output:
html: the html that corresponds to the xml given
'''
try: try:
rubric_categories = CombinedOpenEndedRubric.extract_rubric_categories(rubric_xml) rubric_categories = self.extract_categories(rubric_xml)
html = system.render_template('open_ended_rubric.html', {'rubric_categories' : rubric_categories}) html = self.system.render_template('open_ended_rubric.html',
{'categories' : rubric_categories,
'has_score': self.has_score,
'view_only': self.view_only})
except: except:
log.exception("Could not parse the rubric.") raise RubricParsingError("[render_rubric] Could not parse the rubric with xml: {0}".format(rubric_xml))
html = rubric_xml
return html return html
@staticmethod def extract_categories(self, element):
def extract_rubric_categories(element):
''' '''
Contstruct a list of categories such that the structure looks like: Contstruct a list of categories such that the structure looks like:
[ { category: "Category 1 Name", [ { category: "Category 1 Name",
...@@ -28,17 +45,18 @@ class CombinedOpenEndedRubric(object): ...@@ -28,17 +45,18 @@ class CombinedOpenEndedRubric(object):
{text: "Option 3 Name", points: 2]}] {text: "Option 3 Name", points: 2]}]
''' '''
if isinstance(element, basestring):
element = etree.fromstring(element) element = etree.fromstring(element)
categories = [] categories = []
for category in element: for category in element:
if category.tag != 'category': if category.tag != 'category':
raise Exception("[capa.inputtypes.extract_categories] Expected a <category> tag: got {0} instead".format(category.tag)) raise RubricParsingError("[extract_categories] Expected a <category> tag: got {0} instead".format(category.tag))
else: else:
categories.append(CombinedOpenEndedRubric.extract_category(category)) categories.append(self.extract_category(category))
return categories return categories
@staticmethod
def extract_category(category): def extract_category(self, category):
''' '''
construct an individual category construct an individual category
{category: "Category 1 Name", {category: "Category 1 Name",
...@@ -47,42 +65,33 @@ class CombinedOpenEndedRubric(object): ...@@ -47,42 +65,33 @@ class CombinedOpenEndedRubric(object):
all sorting and auto-point generation occurs in this function all sorting and auto-point generation occurs in this function
''' '''
has_score=False
descriptionxml = category[0] descriptionxml = category[0]
scorexml = category[1]
if scorexml.tag == "option":
optionsxml = category[1:] optionsxml = category[1:]
else: scorexml = category[1]
score = None
if scorexml.tag == 'score':
score_text = scorexml.text
optionsxml = category[2:] optionsxml = category[2:]
has_score=True score = int(score_text)
self.has_score = True
# if we are missing the score tag and we are expecting one
elif self.has_score:
raise RubricParsingError("[extract_category] Category {0} is missing a score".format(descriptionxml.text))
# parse description # parse description
if descriptionxml.tag != 'description': if descriptionxml.tag != 'description':
raise Exception("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag)) raise RubricParsingError("[extract_category]: expected description tag, got {0} instead".format(descriptionxml.tag))
if has_score:
if scorexml.tag != 'score':
raise Exception("[extract_category]: expected score tag, got {0} instead".format(scorexml.tag))
for option in optionsxml:
if option.tag != "option":
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag))
description = descriptionxml.text description = descriptionxml.text
if has_score:
score = int(scorexml.text)
else:
score = 0
cur_points = 0 cur_points = 0
options = [] options = []
autonumbering = True autonumbering = True
# parse options # parse options
for option in optionsxml: for option in optionsxml:
if option.tag != 'option': if option.tag != 'option':
raise Exception("[extract_category]: expected option tag, got {0} instead".format(option.tag)) raise RubricParsingError("[extract_category]: expected option tag, got {0} instead".format(option.tag))
else: else:
pointstr = option.get("points") pointstr = option.get("points")
if pointstr: if pointstr:
...@@ -91,25 +100,24 @@ class CombinedOpenEndedRubric(object): ...@@ -91,25 +100,24 @@ class CombinedOpenEndedRubric(object):
try: try:
points = int(pointstr) points = int(pointstr)
except ValueError: except ValueError:
raise Exception("[extract_category]: expected points to have int, got {0} instead".format(pointstr)) raise RubricParsingError("[extract_category]: expected points to have int, got {0} instead".format(pointstr))
elif autonumbering: elif autonumbering:
# use the generated one if we're in the right mode # use the generated one if we're in the right mode
points = cur_points points = cur_points
cur_points = cur_points + 1 cur_points = cur_points + 1
else: else:
raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly dfined.") raise Exception("[extract_category]: missing points attribute. Cannot continue to auto-create points values after a points value is explicitly defined.")
selected = score == points
optiontext = option.text optiontext = option.text
selected = False options.append({'text': option.text, 'points': points, 'selected': selected})
if has_score:
if points == score:
selected = True
options.append({'text': option.text, 'points': points, 'selected' : selected})
# sort and check for duplicates # sort and check for duplicates
options = sorted(options, key=lambda option: option['points']) options = sorted(options, key=lambda option: option['points'])
CombinedOpenEndedRubric.validate_options(options) CombinedOpenEndedRubric.validate_options(options)
return {'description': description, 'options': options, 'score' : score, 'has_score' : has_score} return {'description': description, 'options': options}
@staticmethod @staticmethod
def validate_options(options): def validate_options(options):
...@@ -117,12 +125,12 @@ class CombinedOpenEndedRubric(object): ...@@ -117,12 +125,12 @@ class CombinedOpenEndedRubric(object):
Validates a set of options. This can and should be extended to filter out other bad edge cases Validates a set of options. This can and should be extended to filter out other bad edge cases
''' '''
if len(options) == 0: if len(options) == 0:
raise Exception("[extract_category]: no options associated with this category") raise RubricParsingError("[extract_category]: no options associated with this category")
if len(options) == 1: if len(options) == 1:
return return
prev = options[0]['points'] prev = options[0]['points']
for option in options[1:]: for option in options[1:]:
if prev == option['points']: if prev == option['points']:
raise Exception("[extract_category]: found duplicate point values between two different options") raise RubricParsingError("[extract_category]: found duplicate point values between two different options")
else: else:
prev = option['points'] prev = option['points']
...@@ -20,6 +20,7 @@ h2 { ...@@ -20,6 +20,7 @@ h2 {
color: darken($error-red, 10%); color: darken($error-red, 10%);
} }
section.problem { section.problem {
@media print { @media print {
display: block; display: block;
...@@ -756,4 +757,49 @@ section.problem { ...@@ -756,4 +757,49 @@ section.problem {
} }
} }
} }
.rubric {
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px;
margin: 10px 0px;
height: 100%;
}
th {
padding: 5px;
margin: 5px;
}
label,
.view-only {
margin:3px;
position: relative;
padding: 15px;
width: 150px;
height:100%;
display: inline-block;
min-height: 50px;
min-width: 50px;
background-color: #CCC;
font-size: .9em;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade {
background: #666;
color: white;
}
input[type=radio]:checked + label {
background: #666;
color: white; }
input[class='score-selection'] {
display: none;
}
}
} }
...@@ -37,9 +37,13 @@ section.combined-open-ended { ...@@ -37,9 +37,13 @@ section.combined-open-ended {
.result-container .result-container
{ {
float:left; float:left;
width: 93%; width: 100%;
position:relative; position:relative;
} }
h4
{
margin-bottom:10px;
}
} }
section.combined-open-ended-status { section.combined-open-ended-status {
...@@ -49,15 +53,19 @@ section.combined-open-ended-status { ...@@ -49,15 +53,19 @@ section.combined-open-ended-status {
color: #2C2C2C; color: #2C2C2C;
font-family: monospace; font-family: monospace;
font-size: 1em; font-size: 1em;
padding-top: 10px; padding: 10px;
.show-results {
margin-top: .3em;
text-align:right;
}
.show-results-button {
font: 1em monospace;
}
} }
.statusitem-current { .statusitem-current {
background-color: #BEBEBE; background-color: #d4d4d4;
color: #2C2C2C; color: #222;
font-family: monospace;
font-size: 1em;
padding-top: 10px;
} }
span { span {
...@@ -93,6 +101,7 @@ section.combined-open-ended-status { ...@@ -93,6 +101,7 @@ section.combined-open-ended-status {
div.result-container { div.result-container {
.evaluation { .evaluation {
p { p {
margin-bottom: 1px; margin-bottom: 1px;
} }
...@@ -104,6 +113,7 @@ div.result-container { ...@@ -104,6 +113,7 @@ div.result-container {
} }
.evaluation-response { .evaluation-response {
margin-bottom: 10px;
header { header {
text-align: right; text-align: right;
a { a {
...@@ -134,6 +144,7 @@ div.result-container { ...@@ -134,6 +144,7 @@ div.result-container {
} }
.external-grader-message { .external-grader-message {
margin-bottom: 5px;
section { section {
padding-left: 20px; padding-left: 20px;
background-color: #FAFAFA; background-color: #FAFAFA;
...@@ -141,6 +152,7 @@ div.result-container { ...@@ -141,6 +152,7 @@ div.result-container {
font-family: monospace; font-family: monospace;
font-size: 1em; font-size: 1em;
padding-top: 10px; padding-top: 10px;
padding-bottom:30px;
header { header {
font-size: 1.4em; font-size: 1.4em;
} }
...@@ -221,12 +233,13 @@ div.result-container { ...@@ -221,12 +233,13 @@ div.result-container {
div.result-container, section.open-ended-child { div.result-container, section.open-ended-child {
.rubric { .rubric {
margin-bottom:25px;
tr { tr {
margin:10px 0px; margin:10px 0px;
height: 100%; height: 100%;
} }
td { td {
padding: 20px 0px; padding: 20px 0px 25px 0px;
margin: 10px 0px; margin: 10px 0px;
height: 100%; height: 100%;
} }
...@@ -236,16 +249,16 @@ div.result-container, section.open-ended-child { ...@@ -236,16 +249,16 @@ div.result-container, section.open-ended-child {
} }
label, label,
.view-only { .view-only {
margin:10px; margin:2px;
position: relative; position: relative;
padding: 15px; padding: 10px 15px 25px 15px;
width: 200px; width: 145px;
height:100%; height:100%;
display: inline-block; display: inline-block;
min-height: 50px; min-height: 50px;
min-width: 50px; min-width: 50px;
background-color: #CCC; background-color: #CCC;
font-size: 1em; font-size: .85em;
} }
.grade { .grade {
position: absolute; position: absolute;
...@@ -257,12 +270,6 @@ div.result-container, section.open-ended-child { ...@@ -257,12 +270,6 @@ div.result-container, section.open-ended-child {
background: #666; background: #666;
color: white; color: white;
} }
input[type=radio]:checked + label {
background: #666;
color: white; }
input[class='score-selection'] {
display: none;
}
} }
} }
...@@ -461,7 +468,6 @@ section.open-ended-child { ...@@ -461,7 +468,6 @@ section.open-ended-child {
p { p {
line-height: 20px; line-height: 20px;
text-transform: capitalize;
margin-bottom: 0; margin-bottom: 0;
float: left; float: left;
} }
...@@ -598,13 +604,15 @@ section.open-ended-child { ...@@ -598,13 +604,15 @@ section.open-ended-child {
} }
} }
div.open-ended-alert { div.open-ended-alert,
.save_message {
padding: 8px 12px; padding: 8px 12px;
border: 1px solid #EBE8BF; border: 1px solid #EBE8BF;
border-radius: 3px; border-radius: 3px;
background: #FFFCDD; background: #FFFCDD;
font-size: 0.9em; font-size: 0.9em;
margin-top: 10px; margin-top: 10px;
margin-bottom:5px;
} }
div.capa_reset { div.capa_reset {
...@@ -623,4 +631,31 @@ section.open-ended-child { ...@@ -623,4 +631,31 @@ section.open-ended-child {
font-size: 0.9em; font-size: 0.9em;
} }
.assessment-container {
margin: 40px 0px 30px 0px;
.scoring-container
{
p
{
margin-bottom: 1em;
}
label {
margin: 10px;
padding: 5px;
display: inline-block;
min-width: 50px;
background-color: #CCC;
text-size: 1.5em;
}
input[type=radio]:checked + label {
background: #666;
color: white;
}
input[class='grade-selection'] {
display: none;
}
}
}
} }
...@@ -9,20 +9,34 @@ class @Collapsible ...@@ -9,20 +9,34 @@ class @Collapsible
### ###
el: container el: container
### ###
# standard longform + shortfom pattern
el.find('.longform').hide() el.find('.longform').hide()
el.find('.shortform').append('<a href="#" class="full">See full output</a>') el.find('.shortform').append('<a href="#" class="full">See full output</a>')
# custom longform + shortform text pattern
short_custom = el.find('.shortform-custom')
# set up each one individually
short_custom.each (index, elt) =>
open_text = $(elt).data('open-text')
close_text = $(elt).data('close-text')
$(elt).append("<a href='#' class='full-custom'>"+ open_text + "</a>")
$(elt).find('.full-custom').click (event) => @toggleFull(event, open_text, close_text)
# collapsible pattern
el.find('.collapsible header + section').hide() el.find('.collapsible header + section').hide()
el.find('.full').click @toggleFull
# set up triggers
el.find('.full').click (event) => @toggleFull(event, "See full output", "Hide output")
el.find('.collapsible header a').click @toggleHint el.find('.collapsible header a').click @toggleHint
@toggleFull: (event) => @toggleFull: (event, open_text, close_text) =>
event.preventDefault() event.preventDefault()
$(event.target).parent().siblings().slideToggle() $(event.target).parent().siblings().slideToggle()
$(event.target).parent().parent().toggleClass('open') $(event.target).parent().parent().toggleClass('open')
if $(event.target).text() == 'See full output' if $(event.target).text() == open_text
new_text = 'Hide output' new_text = close_text
else else
new_text = 'See full output' new_text = open_text
$(event.target).text(new_text) $(event.target).text(new_text)
@toggleHint: (event) => @toggleHint: (event) =>
......
...@@ -109,7 +109,8 @@ class @CombinedOpenEnded ...@@ -109,7 +109,8 @@ class @CombinedOpenEnded
@reset_button.hide() @reset_button.hide()
@next_problem_button.hide() @next_problem_button.hide()
@hint_area.attr('disabled', false) @hint_area.attr('disabled', false)
if @child_state == 'done'
@rubric_wrapper.hide()
if @child_type=="openended" if @child_type=="openended"
@skip_button.hide() @skip_button.hide()
if @allow_reset=="True" if @allow_reset=="True"
...@@ -139,6 +140,7 @@ class @CombinedOpenEnded ...@@ -139,6 +140,7 @@ class @CombinedOpenEnded
else else
@submit_button.click @message_post @submit_button.click @message_post
else if @child_state == 'done' else if @child_state == 'done'
@rubric_wrapper.hide()
@answer_area.attr("disabled", true) @answer_area.attr("disabled", true)
@hint_area.attr('disabled', true) @hint_area.attr('disabled', true)
@submit_button.hide() @submit_button.hide()
...@@ -151,7 +153,7 @@ class @CombinedOpenEnded ...@@ -151,7 +153,7 @@ class @CombinedOpenEnded
find_assessment_elements: -> find_assessment_elements: ->
@assessment = @$('select.assessment') @assessment = @$('input[name="grade-selection"]')
find_hint_elements: -> find_hint_elements: ->
@hint_area = @$('textarea.post_assessment') @hint_area = @$('textarea.post_assessment')
...@@ -163,6 +165,7 @@ class @CombinedOpenEnded ...@@ -163,6 +165,7 @@ class @CombinedOpenEnded
$.postWithPrefix "#{@ajax_url}/save_answer", data, (response) => $.postWithPrefix "#{@ajax_url}/save_answer", data, (response) =>
if response.success if response.success
@rubric_wrapper.html(response.rubric_html) @rubric_wrapper.html(response.rubric_html)
@rubric_wrapper.show()
@child_state = 'assessing' @child_state = 'assessing'
@find_assessment_elements() @find_assessment_elements()
@rebind() @rebind()
...@@ -174,7 +177,8 @@ class @CombinedOpenEnded ...@@ -174,7 +177,8 @@ class @CombinedOpenEnded
save_assessment: (event) => save_assessment: (event) =>
event.preventDefault() event.preventDefault()
if @child_state == 'assessing' if @child_state == 'assessing'
data = {'assessment' : @assessment.find(':selected').text()} checked_assessment = @$('input[name="grade-selection"]:checked')
data = {'assessment' : checked_assessment.val()}
$.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) => $.postWithPrefix "#{@ajax_url}/save_assessment", data, (response) =>
if response.success if response.success
@child_state = response.state @child_state = response.state
...@@ -183,6 +187,7 @@ class @CombinedOpenEnded ...@@ -183,6 +187,7 @@ class @CombinedOpenEnded
@hint_wrapper.html(response.hint_html) @hint_wrapper.html(response.hint_html)
@find_hint_elements() @find_hint_elements()
else if @child_state == 'done' else if @child_state == 'done'
@rubric_wrapper.hide()
@message_wrapper.html(response.message_html) @message_wrapper.html(response.message_html)
@rebind() @rebind()
......
...@@ -121,6 +121,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -121,6 +121,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'rubric': rubric_string, 'rubric': rubric_string,
'initial_display': self.initial_display, 'initial_display': self.initial_display,
'answer': self.answer, 'answer': self.answer,
'problem_id': self.display_name
}) })
updated_grader_payload = json.dumps(parsed_grader_payload) updated_grader_payload = json.dumps(parsed_grader_payload)
...@@ -381,7 +382,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -381,7 +382,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
rubric_feedback="" rubric_feedback=""
feedback = self._convert_longform_feedback_to_html(response_items) feedback = self._convert_longform_feedback_to_html(response_items)
if response_items['rubric_scores_complete']==True: if response_items['rubric_scores_complete']==True:
rubric_feedback = CombinedOpenEndedRubric.render_rubric(response_items['rubric_xml'], system) rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_feedback = rubric_renderer.render_rubric(response_items['rubric_xml'])
if not response_items['success']: if not response_items['success']:
return system.render_template("open_ended_error.html", return system.render_template("open_ended_error.html",
...@@ -446,8 +448,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild): ...@@ -446,8 +448,8 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
'success': score_result['success'], 'success': score_result['success'],
'grader_id': score_result['grader_id'][i], 'grader_id': score_result['grader_id'][i],
'submission_id': score_result['submission_id'], 'submission_id': score_result['submission_id'],
'rubric_scores_complete' : score_result['rubric_scores_complete'], 'rubric_scores_complete' : score_result['rubric_scores_complete'][i],
'rubric_xml' : score_result['rubric_xml'], 'rubric_xml' : score_result['rubric_xml'][i],
} }
feedback_items.append(self._format_feedback(new_score_result, system)) feedback_items.append(self._format_feedback(new_score_result, system))
if join_feedback: if join_feedback:
......
...@@ -93,6 +93,7 @@ class OpenEndedChild(object): ...@@ -93,6 +93,7 @@ class OpenEndedChild(object):
self.prompt = static_data['prompt'] self.prompt = static_data['prompt']
self.rubric = static_data['rubric'] self.rubric = static_data['rubric']
self.display_name = static_data['display_name']
# Used for progress / grading. Currently get credit just for # Used for progress / grading. Currently get credit just for
# completion (doesn't matter if you self-assessed correct/incorrect). # completion (doesn't matter if you self-assessed correct/incorrect).
......
...@@ -75,7 +75,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -75,7 +75,7 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
'previous_answer': previous_answer, 'previous_answer': previous_answer,
'ajax_url': system.ajax_url, 'ajax_url': system.ajax_url,
'initial_rubric': self.get_rubric_html(system), 'initial_rubric': self.get_rubric_html(system),
'initial_hint': self.get_hint_html(system), 'initial_hint': "",
'initial_message': self.get_message_html(), 'initial_message': self.get_message_html(),
'state': self.state, 'state': self.state,
'allow_reset': self._allow_reset(), 'allow_reset': self._allow_reset(),
...@@ -122,7 +122,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -122,7 +122,8 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
if self.state == self.INITIAL: if self.state == self.INITIAL:
return '' return ''
rubric_html = CombinedOpenEndedRubric.render_rubric(self.rubric, system) rubric_renderer = CombinedOpenEndedRubric(system, True)
rubric_html = rubric_renderer.render_rubric(self.rubric)
# we'll render it # we'll render it
context = {'rubric': rubric_html, context = {'rubric': rubric_html,
...@@ -235,13 +236,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild): ...@@ -235,13 +236,9 @@ class SelfAssessmentModule(openendedchild.OpenEndedChild):
d = {'success': True, } d = {'success': True, }
if score == self.max_score():
self.change_state(self.DONE) self.change_state(self.DONE)
d['message_html'] = self.get_message_html() d['message_html'] = self.get_message_html()
d['allow_reset'] = self._allow_reset() d['allow_reset'] = self._allow_reset()
else:
self.change_state(self.POST_ASSESSMENT)
d['hint_html'] = self.get_hint_html(system)
d['state'] = self.state d['state'] = self.state
return d return d
......
...@@ -42,7 +42,8 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -42,7 +42,8 @@ class SelfAssessmentTest(unittest.TestCase):
'max_attempts': 10, 'max_attempts': 10,
'rubric': etree.XML(rubric), 'rubric': etree.XML(rubric),
'prompt': prompt, 'prompt': prompt,
'max_score': 1 'max_score': 1,
'display_name': "Name"
} }
module = SelfAssessmentModule(test_system, self.location, module = SelfAssessmentModule(test_system, self.location,
...@@ -56,8 +57,6 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -56,8 +57,6 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(module.state, module.ASSESSING) self.assertEqual(module.state, module.ASSESSING)
module.save_assessment({'assessment': '0'}, test_system) module.save_assessment({'assessment': '0'}, test_system)
self.assertEqual(module.state, module.POST_ASSESSMENT)
module.save_hint({'hint': 'this is a hint'}, test_system)
self.assertEqual(module.state, module.DONE) self.assertEqual(module.state, module.DONE)
d = module.reset({}) d = module.reset({})
......
...@@ -209,6 +209,14 @@ $.widget("ui.draggable", $.ui.mouse, { ...@@ -209,6 +209,14 @@ $.widget("ui.draggable", $.ui.mouse, {
// computation of pageY or scrollTop() or caching of scrollTop at same state as pageY // computation of pageY or scrollTop() or caching of scrollTop at same state as pageY
// btw: known bug in jqueryui http://bugs.jqueryui.com/ticket/5718 // btw: known bug in jqueryui http://bugs.jqueryui.com/ticket/5718
if (this.scrollParent.is(document) && this.cssPosition === 'relative') { if (this.scrollParent.is(document) && this.cssPosition === 'relative') {
// need to catch the original parent having been shoved down during drag (perhaps by
// events)
// update cached originals if it shifted
if (this.offset && this.offset.parent && this.offset.parent.top !== this._getParentOffset().top) {
var deltaY = this.offset.parent.top - this._getParentOffset().top;
this.offset.parent.top = this._getParentOffset().top;
this.originalPageY -= deltaY;
}
this.helper[0].style.top = (event.pageY - this.originalPageY) +"px"; this.helper[0].style.top = (event.pageY - this.originalPageY) +"px";
} }
else this.helper[0].style.top = this.position.top+"px"; else this.helper[0].style.top = this.position.top+"px";
......
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
from certificates.models import CertificateWhitelist
from django.contrib.auth.models import User
class Command(BaseCommand):
help = """
Sets or gets the certificate whitelist for a given
user/course
Add a user to the whitelist for a course
$ ... cert_whitelist --add joe -c "MITx/6.002x/2012_Fall"
Remove a user from the whitelist for a course
$ ... cert_whitelist --del joe -c "MITx/6.002x/2012_Fall"
Print out who is whitelisted for a course
$ ... cert_whitelist -c "MITx/6.002x/2012_Fall"
"""
option_list = BaseCommand.option_list + (
make_option('-a', '--add',
metavar='USER',
dest='add',
default=False,
help='user to add to the certificate whitelist'),
make_option('-d', '--del',
metavar='USER',
dest='del',
default=False,
help='user to remove from the certificate whitelist'),
make_option('-c', '--course-id',
metavar='COURSE_ID',
dest='course_id',
default=False,
help="course id to query"),
)
def handle(self, *args, **options):
course_id = options['course_id']
if not course_id:
raise CommandError("You must specify a course-id")
if options['add'] and options['del']:
raise CommandError("Either remove or add a user, not both")
if options['add'] or options['del']:
user_str = options['add'] or options['del']
if '@' in user_str:
user = User.objects.get(email=user_str)
else:
user = User.objects.get(username=user_str)
cert_whitelist, created = \
CertificateWhitelist.objects.get_or_create(
user=user, course_id=course_id)
if options['add']:
cert_whitelist.whitelist = True
elif options['del']:
cert_whitelist.whitelist = False
cert_whitelist.save()
whitelist = CertificateWhitelist.objects.all()
print "User whitelist for course {0}:\n{1}".format(course_id,
'\n'.join(["{0} {1} {2}".format(
u.user.username, u.user.email, u.whitelist)
for u in whitelist]))
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'CertificateWhitelist'
db.create_table('certificates_certificatewhitelist', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('course_id', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)),
('whitelist', self.gf('django.db.models.fields.BooleanField')(default=False)),
))
db.send_create_signal('certificates', ['CertificateWhitelist'])
def backwards(self, orm):
# Deleting model 'CertificateWhitelist'
db.delete_table('certificates_certificatewhitelist')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'certificates.certificatewhitelist': {
'Meta': {'object_name': 'CertificateWhitelist'},
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'whitelist': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
},
'certificates.generatedcertificate': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'created_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}),
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', 'blank': 'True'}),
'download_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
'error_reason': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '512', 'blank': 'True'}),
'grade': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '5', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
'modified_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
'verify_uuid': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['certificates']
\ No newline at end of file
...@@ -35,6 +35,19 @@ State diagram: ...@@ -35,6 +35,19 @@ State diagram:
v v v v v v
[downloadable] [downloadable] [deleted] [downloadable] [downloadable] [deleted]
Eligibility:
Students are eligible for a certificate if they pass the course
with the following exceptions:
If the student has allow_certificate set to False in the student profile
he will never be issued a certificate.
If the user and course is present in the certificate whitelist table
then the student will be issued a certificate regardless of his grade,
unless he has allow_certificate set to False.
""" """
...@@ -46,8 +59,20 @@ class CertificateStatuses(object): ...@@ -46,8 +59,20 @@ class CertificateStatuses(object):
deleted = 'deleted' deleted = 'deleted'
downloadable = 'downloadable' downloadable = 'downloadable'
notpassing = 'notpassing' notpassing = 'notpassing'
restricted = 'restricted'
error = 'error' error = 'error'
class CertificateWhitelist(models.Model):
"""
Tracks students who are whitelisted, all users
in this table will always qualify for a certificate
regardless of their grade unless they are on the
embargoed country restriction list
(allow_certificate set to False in userprofile).
"""
user = models.ForeignKey(User)
course_id = models.CharField(max_length=255, blank=True, default='')
whitelist = models.BooleanField(default=0)
class GeneratedCertificate(models.Model): class GeneratedCertificate(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
...@@ -87,6 +112,10 @@ def certificate_status_for_student(student, course_id): ...@@ -87,6 +112,10 @@ def certificate_status_for_student(student, course_id):
deleted - The certificate has been deleted. deleted - The certificate has been deleted.
downloadable - The certificate is available for download. downloadable - The certificate is available for download.
notpassing - The student was graded but is not passing notpassing - The student was graded but is not passing
restricted - The student is on the restricted embargo list and
should not be issued a certificate. This will
be set if allow_certificate is set to False in
the userprofile table
If the status is "downloadable", the dictionary also contains If the status is "downloadable", the dictionary also contains
"download_url". "download_url".
......
from certificates.models import GeneratedCertificate from certificates.models import GeneratedCertificate
from certificates.models import certificate_status_for_student from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status from certificates.models import CertificateStatuses as status
from certificates.models import CertificateWhitelist
from courseware import grades, courses from courseware import grades, courses
from django.test.client import RequestFactory from django.test.client import RequestFactory
...@@ -71,6 +72,8 @@ class XQueueCertInterface(object): ...@@ -71,6 +72,8 @@ class XQueueCertInterface(object):
settings.XQUEUE_INTERFACE['django_auth'], settings.XQUEUE_INTERFACE['django_auth'],
requests_auth, requests_auth,
) )
self.whitelist = CertificateWhitelist.objects.all()
self.restricted = UserProfile.objects.filter(allow_certificate=False)
def regen_cert(self, student, course_id): def regen_cert(self, student, course_id):
""" """
...@@ -93,49 +96,7 @@ class XQueueCertInterface(object): ...@@ -93,49 +96,7 @@ class XQueueCertInterface(object):
""" """
VALID_STATUSES = [status.error, status.downloadable] raise NotImplementedError
cert_status = certificate_status_for_student(
student, course_id)['status']
if cert_status in VALID_STATUSES:
# grade the student
course = courses.get_course_by_id(course_id)
grade = grades.grade(student, self.request, course)
profile = UserProfile.objects.get(user=student)
try:
cert = GeneratedCertificate.objects.get(
user=student, course_id=course_id)
except GeneratedCertificate.DoesNotExist:
logger.critical("Attempting to regenerate a certificate"
"for a user that doesn't have one")
raise
if grade['grade'] is not None:
cert.status = status.regenerating
cert.name = profile.name
contents = {
'action': 'regen',
'delete_verify_uuid': cert.verify_uuid,
'delete_download_uuid': cert.download_uuid,
'username': cert.user.username,
'course_id': cert.course_id,
'name': profile.name,
}
key = cert.key
self._send_to_xqueue(contents, key)
cert.save()
else:
cert.status = status.notpassing
cert.name = profile.name
cert.save()
return cert_status
def del_cert(self, student, course_id): def del_cert(self, student, course_id):
...@@ -152,34 +113,7 @@ class XQueueCertInterface(object): ...@@ -152,34 +113,7 @@ class XQueueCertInterface(object):
""" """
VALID_STATUSES = [status.error, status.downloadable] raise NotImplementedError
cert_status = certificate_status_for_student(
student, course_id)['status']
if cert_status in VALID_STATUSES:
try:
cert = GeneratedCertificate.objects.get(
user=student, course_id=course_id)
except GeneratedCertificate.DoesNotExist:
logger.warning("Attempting to delete a certificate"
"for a user that doesn't have one")
raise
cert.status = status.deleting
contents = {
'action': 'delete',
'delete_verify_uuid': cert.verify_uuid,
'delete_download_uuid': cert.download_uuid,
'username': cert.user.username,
}
key = cert.key
self._send_to_xqueue(contents, key)
cert.save()
return cert_status
def add_cert(self, student, course_id): def add_cert(self, student, course_id):
""" """
...@@ -189,13 +123,17 @@ class XQueueCertInterface(object): ...@@ -189,13 +123,17 @@ class XQueueCertInterface(object):
course_id - courseenrollment.course_id (string) course_id - courseenrollment.course_id (string)
Request a new certificate for a student. Request a new certificate for a student.
Will change the certificate status to 'deleting'. Will change the certificate status to 'generating'.
Certificate must be in the 'unavailable', 'error', Certificate must be in the 'unavailable', 'error',
'deleted' or 'generating' state. 'deleted' or 'generating' state.
If a student has a passing grade a request will made If a student has a passing grade or is in the whitelist
for a new cert table for the course a request will made for a new cert.
If a student has allow_certificate set to False in the
userprofile table the status will change to 'restricted'
If a student does not have a passing grade the status If a student does not have a passing grade the status
will change to status.notpassing will change to status.notpassing
...@@ -214,29 +152,40 @@ class XQueueCertInterface(object): ...@@ -214,29 +152,40 @@ class XQueueCertInterface(object):
if cert_status in VALID_STATUSES: if cert_status in VALID_STATUSES:
# grade the student # grade the student
course = courses.get_course_by_id(course_id) course = courses.get_course_by_id(course_id)
grade = grades.grade(student, self.request, course)
profile = UserProfile.objects.get(user=student) profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create( cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id) user=student, course_id=course_id)
if grade['grade'] is not None: grade = grades.grade(student, self.request, course)
cert_status = status.generating is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists()
if is_whitelisted or grade['grade'] is not None:
key = make_hashkey(random.random()) key = make_hashkey(random.random())
cert.status = cert_status
cert.grade = grade['percent'] cert.grade = grade['percent']
cert.user = student cert.user = student
cert.course_id = course_id cert.course_id = course_id
cert.key = key cert.key = key
cert.name = profile.name cert.name = profile.name
# check to see whether the student is on the
# the embargoed country restricted list
# otherwise, put a new certificate request
# on the queue
if self.restricted.filter(user=student).exists():
cert.status = status.restricted
else:
contents = { contents = {
'action': 'create', 'action': 'create',
'username': student.username, 'username': student.username,
'course_id': course_id, 'course_id': course_id,
'name': profile.name, 'name': profile.name,
} }
cert.status = status.generating
self._send_to_xqueue(contents, key) self._send_to_xqueue(contents, key)
cert.save() cert.save()
else: else:
......
...@@ -11,6 +11,10 @@ from django.http import HttpResponse, Http404 ...@@ -11,6 +11,10 @@ from django.http import HttpResponse, Http404
from courseware.access import has_access from courseware.access import has_access
from util.json_request import expect_json from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric, RubricParsingError
from lxml import etree
from mitxmako.shortcuts import render_to_string
from xmodule.x_module import ModuleSystem
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -27,6 +31,7 @@ class GradingService(object): ...@@ -27,6 +31,7 @@ class GradingService(object):
self.url = config['url'] self.url = config['url']
self.login_url = self.url + '/login/' self.login_url = self.url + '/login/'
self.session = requests.session() self.session = requests.session()
self.system = ModuleSystem(None, None, None, render_to_string, None)
def _login(self): def _login(self):
""" """
...@@ -98,3 +103,33 @@ class GradingService(object): ...@@ -98,3 +103,33 @@ class GradingService(object):
return response return response
def _render_rubric(self, response, view_only=False):
"""
Given an HTTP Response with the key 'rubric', render out the html
required to display the rubric and put it back into the response
returns the updated response as a dictionary that can be serialized later
"""
try:
response_json = json.loads(response)
if 'rubric' in response_json:
rubric = response_json['rubric']
rubric_renderer = CombinedOpenEndedRubric(self.system, False)
rubric_html = rubric_renderer.render_rubric(rubric)
response_json['rubric'] = rubric_html
return response_json
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError, RubricParsingError:
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
return {'success': False,
'error': 'Error displaying submission'}
except ValueError:
log.exception("Error parsing response: {0}".format(response))
return {'success': False,
'error': "Error displaying submission"}
...@@ -20,7 +20,9 @@ from grading_service import GradingServiceError ...@@ -20,7 +20,9 @@ from grading_service import GradingServiceError
from courseware.access import has_access from courseware.access import has_access
from util.json_request import expect_json from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.combined_open_ended_rubric import CombinedOpenEndedRubric
from student.models import unique_id_for_user from student.models import unique_id_for_user
from lxml import etree
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -84,15 +86,17 @@ class PeerGradingService(GradingService): ...@@ -84,15 +86,17 @@ class PeerGradingService(GradingService):
def get_next_submission(self, problem_location, grader_id): def get_next_submission(self, problem_location, grader_id):
response = self.get(self.get_next_submission_url, response = self.get(self.get_next_submission_url,
{'location': problem_location, 'grader_id': grader_id}) {'location': problem_location, 'grader_id': grader_id})
return response return json.dumps(self._render_rubric(response))
def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key): def save_grade(self, location, grader_id, submission_id, score, feedback, submission_key, rubric_scores):
data = {'grader_id' : grader_id, data = {'grader_id' : grader_id,
'submission_id' : submission_id, 'submission_id' : submission_id,
'score' : score, 'score' : score,
'feedback' : feedback, 'feedback' : feedback,
'submission_key': submission_key, 'submission_key': submission_key,
'location': location} 'location': location,
'rubric_scores': rubric_scores,
'rubric_scores_complete': True}
return self.post(self.save_grade_url, data) return self.post(self.save_grade_url, data)
def is_student_calibrated(self, problem_location, grader_id): def is_student_calibrated(self, problem_location, grader_id):
...@@ -101,15 +105,19 @@ class PeerGradingService(GradingService): ...@@ -101,15 +105,19 @@ class PeerGradingService(GradingService):
def show_calibration_essay(self, problem_location, grader_id): def show_calibration_essay(self, problem_location, grader_id):
params = {'problem_id' : problem_location, 'student_id': grader_id} params = {'problem_id' : problem_location, 'student_id': grader_id}
return self.get(self.show_calibration_essay_url, params) response = self.get(self.show_calibration_essay_url, params)
return json.dumps(self._render_rubric(response))
def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key, score, feedback): def save_calibration_essay(self, problem_location, grader_id, calibration_essay_id, submission_key,
score, feedback, rubric_scores):
data = {'location': problem_location, data = {'location': problem_location,
'student_id': grader_id, 'student_id': grader_id,
'calibration_essay_id': calibration_essay_id, 'calibration_essay_id': calibration_essay_id,
'submission_key': submission_key, 'submission_key': submission_key,
'score': score, 'score': score,
'feedback': feedback} 'feedback': feedback,
'rubric_scores[]': rubric_scores,
'rubric_scores_complete': True}
return self.post(self.save_calibration_essay_url, data) return self.post(self.save_calibration_essay_url, data)
def get_problem_list(self, course_id, grader_id): def get_problem_list(self, course_id, grader_id):
...@@ -196,7 +204,7 @@ def get_next_submission(request, course_id): ...@@ -196,7 +204,7 @@ def get_next_submission(request, course_id):
mimetype="application/json") mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}" log.exception("Error getting next submission. server url: {0} location: {1}, grader_id: {2}"
.format(staff_grading_service().url, location, grader_id)) .format(peer_grading_service().url, location, grader_id))
return json.dumps({'success': False, return json.dumps({'success': False,
'error': 'Could not connect to grading service'}) 'error': 'Could not connect to grading service'})
...@@ -216,7 +224,7 @@ def save_grade(request, course_id): ...@@ -216,7 +224,7 @@ def save_grade(request, course_id):
error: if there was an error in the submission, this is the error message error: if there was an error in the submission, this is the error message
""" """
_check_post(request) _check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback']) required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
success, message = _check_required(request, required) success, message = _check_required(request, required)
if not success: if not success:
return _err_response(message) return _err_response(message)
...@@ -227,14 +235,15 @@ def save_grade(request, course_id): ...@@ -227,14 +235,15 @@ def save_grade(request, course_id):
score = p['score'] score = p['score']
feedback = p['feedback'] feedback = p['feedback']
submission_key = p['submission_key'] submission_key = p['submission_key']
rubric_scores = p.getlist('rubric_scores[]')
try: try:
response = peer_grading_service().save_grade(location, grader_id, submission_id, response = peer_grading_service().save_grade(location, grader_id, submission_id,
score, feedback, submission_key) score, feedback, submission_key, rubric_scores)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2}, log.exception("""Error saving grade. server url: {0}, location: {1}, submission_id:{2},
submission_key: {3}, score: {4}""" submission_key: {3}, score: {4}"""
.format(staff_grading_service().url, .format(peer_grading_service().url,
location, submission_id, submission_key, score) location, submission_id, submission_key, score)
) )
return json.dumps({'success': False, return json.dumps({'success': False,
...@@ -273,7 +282,7 @@ def is_student_calibrated(request, course_id): ...@@ -273,7 +282,7 @@ def is_student_calibrated(request, course_id):
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}" log.exception("Error from grading service. server url: {0}, grader_id: {0}, location: {1}"
.format(staff_grading_service().url, grader_id, location)) .format(peer_grading_service().url, grader_id, location))
return json.dumps({'success': False, return json.dumps({'success': False,
'error': 'Could not connect to grading service'}) 'error': 'Could not connect to grading service'})
...@@ -317,9 +326,15 @@ def show_calibration_essay(request, course_id): ...@@ -317,9 +326,15 @@ def show_calibration_essay(request, course_id):
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error from grading service. server url: {0}, location: {0}" log.exception("Error from grading service. server url: {0}, location: {0}"
.format(staff_grading_service().url, location)) .format(peer_grading_service().url, location))
return json.dumps({'success': False, return json.dumps({'success': False,
'error': 'Could not connect to grading service'}) 'error': 'Could not connect to grading service'})
# if we can't parse the rubric into HTML,
except etree.XMLSyntaxError:
log.exception("Cannot parse rubric string. Raw string: {0}"
.format(rubric))
return json.dumps({'success': False,
'error': 'Error displaying submission'})
def save_calibration_essay(request, course_id): def save_calibration_essay(request, course_id):
...@@ -341,7 +356,7 @@ def save_calibration_essay(request, course_id): ...@@ -341,7 +356,7 @@ def save_calibration_essay(request, course_id):
""" """
_check_post(request) _check_post(request)
required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback']) required = set(['location', 'submission_id', 'submission_key', 'score', 'feedback', 'rubric_scores[]'])
success, message = _check_required(request, required) success, message = _check_required(request, required)
if not success: if not success:
return _err_response(message) return _err_response(message)
...@@ -352,9 +367,11 @@ def save_calibration_essay(request, course_id): ...@@ -352,9 +367,11 @@ def save_calibration_essay(request, course_id):
submission_key = p['submission_key'] submission_key = p['submission_key']
score = p['score'] score = p['score']
feedback = p['feedback'] feedback = p['feedback']
rubric_scores = p.getlist('rubric_scores[]')
try: try:
response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id, submission_key, score, feedback) response = peer_grading_service().save_calibration_essay(location, grader_id, calibration_essay_id,
submission_key, score, feedback, rubric_scores)
return HttpResponse(response, mimetype="application/json") return HttpResponse(response, mimetype="application/json")
except GradingServiceError: except GradingServiceError:
log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id)) log.exception("Error saving calibration grade, location: {0}, submission_id: {1}, submission_key: {2}, grader_id: {3}".format(location, submission_id, submission_key, grader_id))
......
...@@ -17,6 +17,8 @@ from courseware.access import has_access ...@@ -17,6 +17,8 @@ from courseware.access import has_access
from util.json_request import expect_json from util.json_request import expect_json
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from student.models import unique_id_for_user from student.models import unique_id_for_user
from xmodule.x_module import ModuleSystem
from mitxmako.shortcuts import render_to_string
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -46,14 +48,14 @@ class MockStaffGradingService(object): ...@@ -46,14 +48,14 @@ class MockStaffGradingService(object):
self.cnt += 1 self.cnt += 1
return json.dumps({'success': True, return json.dumps({'success': True,
'problem_list': [ 'problem_list': [
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1', \ json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo1',
'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'min_for_ml': 10}), 'problem_name': "Problem 1", 'num_graded': 3, 'num_pending': 5, 'min_for_ml': 10}),
json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2', \ json.dumps({'location': 'i4x://MITx/3.091x/problem/open_ended_demo2',
'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'min_for_ml': 10}) 'problem_name': "Problem 2", 'num_graded': 1, 'num_pending': 5, 'min_for_ml': 10})
]}) ]})
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores):
return self.get_next(course_id, 'fake location', grader_id) return self.get_next(course_id, 'fake location', grader_id)
...@@ -107,12 +109,13 @@ class StaffGradingService(GradingService): ...@@ -107,12 +109,13 @@ class StaffGradingService(GradingService):
Raises: Raises:
GradingServiceError: something went wrong with the connection. GradingServiceError: something went wrong with the connection.
""" """
return self.get(self.get_next_url, response = self.get(self.get_next_url,
params={'location': location, params={'location': location,
'grader_id': grader_id}) 'grader_id': grader_id})
return json.dumps(self._render_rubric(response))
def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped): def save_grade(self, course_id, grader_id, submission_id, score, feedback, skipped, rubric_scores):
""" """
Save a score and feedback for a submission. Save a score and feedback for a submission.
...@@ -129,7 +132,9 @@ class StaffGradingService(GradingService): ...@@ -129,7 +132,9 @@ class StaffGradingService(GradingService):
'score': score, 'score': score,
'feedback': feedback, 'feedback': feedback,
'grader_id': grader_id, 'grader_id': grader_id,
'skipped': skipped} 'skipped': skipped,
'rubric_scores': rubric_scores,
'rubric_scores_complete': True}
return self.post(self.save_grade_url, data=data) return self.post(self.save_grade_url, data=data)
...@@ -143,6 +148,7 @@ class StaffGradingService(GradingService): ...@@ -143,6 +148,7 @@ class StaffGradingService(GradingService):
# importing this file doesn't create objects that may not have the right config # importing this file doesn't create objects that may not have the right config
_service = None _service = None
def staff_grading_service(): def staff_grading_service():
""" """
Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True, Return a staff grading service instance--if settings.MOCK_STAFF_GRADING is True,
...@@ -286,7 +292,7 @@ def save_grade(request, course_id): ...@@ -286,7 +292,7 @@ def save_grade(request, course_id):
if request.method != 'POST': if request.method != 'POST':
raise Http404 raise Http404
required = set(['score', 'feedback', 'submission_id', 'location']) required = set(['score', 'feedback', 'submission_id', 'location', 'rubric_scores[]'])
actual = set(request.POST.keys()) actual = set(request.POST.keys())
missing = required - actual missing = required - actual
if len(missing) > 0: if len(missing) > 0:
...@@ -299,13 +305,15 @@ def save_grade(request, course_id): ...@@ -299,13 +305,15 @@ def save_grade(request, course_id):
location = p['location'] location = p['location']
skipped = 'skipped' in p skipped = 'skipped' in p
try: try:
result_json = staff_grading_service().save_grade(course_id, result_json = staff_grading_service().save_grade(course_id,
grader_id, grader_id,
p['submission_id'], p['submission_id'],
p['score'], p['score'],
p['feedback'], p['feedback'],
skipped) skipped,
p.getlist('rubric_scores[]'))
except GradingServiceError: except GradingServiceError:
log.exception("Error saving grade") log.exception("Error saving grade")
return _err_response('Could not connect to grading service') return _err_response('Could not connect to grading service')
......
...@@ -94,7 +94,8 @@ class TestStaffGradingService(ct.PageLoader): ...@@ -94,7 +94,8 @@ class TestStaffGradingService(ct.PageLoader):
data = {'score': '12', data = {'score': '12',
'feedback': 'great!', 'feedback': 'great!',
'submission_id': '123', 'submission_id': '123',
'location': self.location} 'location': self.location,
'rubric_scores[]': ['1', '2']}
r = self.check_for_post_code(200, url, data) r = self.check_for_post_code(200, url, data)
d = json.loads(r.content) d = json.loads(r.content)
self.assertTrue(d['success'], str(d)) self.assertTrue(d['success'], str(d))
......
...@@ -10,4 +10,18 @@ class PeerGrading ...@@ -10,4 +10,18 @@ class PeerGrading
@message_container = $('.message-container') @message_container = $('.message-container')
@message_container.toggle(not @message_container.is(':empty')) @message_container.toggle(not @message_container.is(':empty'))
@problem_list = $('.problem-list')
@construct_progress_bar()
construct_progress_bar: () =>
problems = @problem_list.find('tr').next()
problems.each( (index, element) =>
problem = $(element)
progress_bar = problem.find('.progress-bar')
bar_value = parseInt(problem.data('graded'))
bar_max = parseInt(problem.data('required')) + bar_value
progress_bar.progressbar({value: bar_value, max: bar_max})
)
$(document).ready(() -> new PeerGrading()) $(document).ready(() -> new PeerGrading())
...@@ -56,13 +56,41 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t ...@@ -56,13 +56,41 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p> <p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
''' '''
rubric: ''' rubric: '''
<ul> <table class="rubric"><tbody><tr><th>Purpose</th>
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
</ul>
<p>Please score your response according to how many of the above components you identified:</p> <td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
</td>
</tr><tr><th>Organization</th>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
</td>
</tr></tbody></table>
''' '''
max_score: 4 max_score: 4
else if cmd == 'get_next_submission' else if cmd == 'get_next_submission'
...@@ -82,13 +110,41 @@ Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim q ...@@ -82,13 +110,41 @@ Curabitur tristique purus ac arcu consequat cursus. Cras diam felis, dignissim q
<p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p> <p>This is a self-assessed open response question. Please use as much space as you need in the box below to answer the question.</p>
''' '''
rubric: ''' rubric: '''
<ul> <table class="rubric"><tbody><tr><th>Purpose</th>
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li> <td>
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li> <input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
</ul> </td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
</td>
</tr><tr><th>Organization</th>
<p>Please score your response according to how many of the above components you identified:</p> <td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
</td>
</tr></tbody></table>
''' '''
max_score: 4 max_score: 4
else if cmd == 'save_calibration_essay' else if cmd == 'save_calibration_essay'
...@@ -137,7 +193,8 @@ class PeerGradingProblem ...@@ -137,7 +193,8 @@ class PeerGradingProblem
@feedback_area = $('.feedback-area') @feedback_area = $('.feedback-area')
@score_selection_container = $('.score-selection-container') @score_selection_container = $('.score-selection-container')
@score = null @rubric_selection_container = $('.rubric-selection-container')
@grade = null
@calibration = null @calibration = null
@submit_button = $('.submit-button') @submit_button = $('.submit-button')
...@@ -175,9 +232,23 @@ class PeerGradingProblem ...@@ -175,9 +232,23 @@ class PeerGradingProblem
fetch_submission_essay: () => fetch_submission_essay: () =>
@backend.post('get_next_submission', {location: @location}, @render_submission) @backend.post('get_next_submission', {location: @location}, @render_submission)
# finds the scores for each rubric category
get_score_list: () =>
# find the number of categories:
num_categories = $('table.rubric tr').length
score_lst = []
# get the score for each one
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
score_lst.push(score)
return score_lst
construct_data: () -> construct_data: () ->
data = data =
score: @score rubric_scores: @get_score_list()
score: @grade
location: @location location: @location
submission_id: @essay_id_input.val() submission_id: @essay_id_input.val()
submission_key: @submission_key_input.val() submission_key: @submission_key_input.val()
...@@ -244,8 +315,16 @@ class PeerGradingProblem ...@@ -244,8 +315,16 @@ class PeerGradingProblem
# called after a grade is selected on the interface # called after a grade is selected on the interface
graded_callback: (event) => graded_callback: (event) =>
@grading_message.hide() @grade = $("input[name='grade-selection']:checked").val()
@score = event.target.value if @grade == undefined
return
# check to see whether or not any categories have not been scored
num_categories = $('table.rubric tr').length
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
if score == undefined
return
# show button if we have scores for all categories
@show_submit_button() @show_submit_button()
...@@ -322,7 +401,7 @@ class PeerGradingProblem ...@@ -322,7 +401,7 @@ class PeerGradingProblem
@submission_container.append(@make_paragraphs(response.student_response)) @submission_container.append(@make_paragraphs(response.student_response))
@prompt_container.html(response.prompt) @prompt_container.html(response.prompt)
@rubric_container.html(response.rubric) @rubric_selection_container.html(response.rubric)
@submission_key_input.val(response.submission_key) @submission_key_input.val(response.submission_key)
@essay_id_input.val(response.submission_id) @essay_id_input.val(response.submission_id)
@setup_score_selection(response.max_score) @setup_score_selection(response.max_score)
...@@ -336,10 +415,10 @@ class PeerGradingProblem ...@@ -336,10 +415,10 @@ class PeerGradingProblem
# display correct grade # display correct grade
@calibration_feedback_panel.slideDown() @calibration_feedback_panel.slideDown()
calibration_wrapper = $('.calibration-feedback-wrapper') calibration_wrapper = $('.calibration-feedback-wrapper')
calibration_wrapper.html("<p>The score you gave was: #{@score}. The actual score is: #{response.actual_score}</p>") calibration_wrapper.html("<p>The score you gave was: #{@grade}. The actual score is: #{response.actual_score}</p>")
score = parseInt(@score) score = parseInt(@grade)
actual_score = parseInt(response.actual_score) actual_score = parseInt(response.actual_score)
if score == actual_score if score == actual_score
...@@ -366,8 +445,12 @@ class PeerGradingProblem ...@@ -366,8 +445,12 @@ class PeerGradingProblem
@submit_button.show() @submit_button.show()
setup_score_selection: (max_score) => setup_score_selection: (max_score) =>
# first, get rid of all the old inputs, if any. # first, get rid of all the old inputs, if any.
@score_selection_container.html('Choose score: ') @score_selection_container.html("""
<h3>Overall Score</h3>
<p>Choose an overall score for this submission.</p>
""")
# Now create new labels and inputs for each possible score. # Now create new labels and inputs for each possible score.
for score in [0..max_score] for score in [0..max_score]
...@@ -375,12 +458,13 @@ class PeerGradingProblem ...@@ -375,12 +458,13 @@ class PeerGradingProblem
label = """<label for="#{id}">#{score}</label>""" label = """<label for="#{id}">#{score}</label>"""
input = """ input = """
<input type="radio" name="score-selection" id="#{id}" value="#{score}"/> <input type="radio" name="grade-selection" id="#{id}" value="#{score}"/>
""" # " fix broken parsing in emacs """ # " fix broken parsing in emacs
@score_selection_container.append(input + label) @score_selection_container.append(input + label)
# And now hook up an event handler again # And now hook up an event handler again
$("input[name='score-selection']").change @graded_callback $("input[name='score-selection']").change @graded_callback
$("input[name='grade-selection']").change @graded_callback
......
...@@ -42,14 +42,41 @@ class StaffGradingBackend ...@@ -42,14 +42,41 @@ class StaffGradingBackend
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham. The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
''' '''
rubric: ''' rubric: '''
<ul> <table class="rubric"><tbody><tr><th>Purpose</th>
<li>Metals tend to be good electronic conductors, meaning that they have a large number of electrons which are able to access empty (mobile) energy states within the material.</li>
<li>Sodium has a half-filled s-band, so there are a number of empty states immediately above the highest occupied energy levels within the band.</li>
<li>Magnesium has a full s-band, but the the s-band and p-band overlap in magnesium. Thus are still a large number of available energy states immediately above the s-band highest occupied energy level.</li>
</ul>
<p>Please score your response according to how many of the above components you identified:</p> <td>
''' <input type="radio" class="score-selection" name="score-selection-0" id="score-0-0" value="0"><label for="score-0-0">No product</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-1" value="1"><label for="score-0-1">Unclear purpose or main idea</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-2" value="2"><label for="score-0-2">Communicates an identifiable purpose and/or main idea for an audience</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-0" id="score-0-3" value="3"><label for="score-0-3">Achieves a clear and distinct purpose for a targeted audience and communicates main ideas with effectively used techniques to introduce and represent ideas and insights</label>
</td>
</tr><tr><th>Organization</th>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-0" value="0"><label for="score-1-0">No product</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-1" value="1"><label for="score-1-1">Organization is unclear; introduction, body, and/or conclusion are underdeveloped, missing or confusing.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-2" value="2"><label for="score-1-2">Organization is occasionally unclear; introduction, body or conclusion may be underdeveloped.</label>
</td>
<td>
<input type="radio" class="score-selection" name="score-selection-1" id="score-1-3" value="3"><label for="score-1-3">Organization is clear and easy to follow; introduction, body and conclusion are defined and aligned with purpose.</label>
</td>
</tr></tbody></table>'''
submission_id: @mock_cnt submission_id: @mock_cnt
max_score: 2 + @mock_cnt % 3 max_score: 2 + @mock_cnt % 3
ml_error_info : 'ML accuracy info: ' + @mock_cnt ml_error_info : 'ML accuracy info: ' + @mock_cnt
...@@ -134,12 +161,11 @@ class StaffGrading ...@@ -134,12 +161,11 @@ class StaffGrading
@submission_container = $('.submission-container') @submission_container = $('.submission-container')
@submission_wrapper = $('.submission-wrapper') @submission_wrapper = $('.submission-wrapper')
@rubric_container = $('.rubric-container')
@rubric_wrapper = $('.rubric-wrapper')
@grading_wrapper = $('.grading-wrapper') @grading_wrapper = $('.grading-wrapper')
@feedback_area = $('.feedback-area') @feedback_area = $('.feedback-area')
@score_selection_container = $('.score-selection-container') @score_selection_container = $('.score-selection-container')
@grade_selection_container = $('.grade-selection-container')
@submit_button = $('.submit-button') @submit_button = $('.submit-button')
@action_button = $('.action-button') @action_button = $('.action-button')
...@@ -166,8 +192,9 @@ class StaffGrading ...@@ -166,8 +192,9 @@ class StaffGrading
@min_for_ml = 0 @min_for_ml = 0
@num_graded = 0 @num_graded = 0
@num_pending = 0 @num_pending = 0
@score_lst = []
@grade = null
@score = null
@problems = null @problems = null
# action handlers # action handlers
...@@ -182,30 +209,53 @@ class StaffGrading ...@@ -182,30 +209,53 @@ class StaffGrading
setup_score_selection: => setup_score_selection: =>
# first, get rid of all the old inputs, if any. # first, get rid of all the old inputs, if any.
@score_selection_container.html('Choose score: ') @grade_selection_container.html("""
<h3>Overall Score</h3>
<p>Choose an overall score for this submission.</p>
""")
# Now create new labels and inputs for each possible score. # Now create new labels and inputs for each possible score.
for score in [0..@max_score] for score in [0..@max_score]
id = 'score-' + score id = 'score-' + score
label = """<label for="#{id}">#{score}</label>""" label = """<label for="#{id}">#{score}</label>"""
input = """ input = """
<input type="radio" name="score-selection" id="#{id}" value="#{score}"/> <input type="radio" class="grade-selection" name="grade-selection" id="#{id}" value="#{score}"/>
""" # " fix broken parsing in emacs """ # " fix broken parsing in emacs
@score_selection_container.append(input + label) @grade_selection_container.append(input + label)
$('.grade-selection').click => @graded_callback()
# And now hook up an event handler again
$("input[name='score-selection']").change @graded_callback @score_selection_container.html(@rubric)
$('.score-selection').click => @graded_callback()
graded_callback: () =>
@grade = $("input[name='grade-selection']:checked").val()
if @grade == undefined
return
# check to see whether or not any categories have not been scored
num_categories = $('table.rubric tr').length
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
if score == undefined
return
# show button if we have scores for all categories
@state = state_graded
@submit_button.show()
set_button_text: (text) => set_button_text: (text) =>
@action_button.attr('value', text) @action_button.attr('value', text)
graded_callback: (event) => # finds the scores for each rubric category
@score = event.target.value get_score_list: () =>
@state = state_graded # find the number of categories:
@message = '' num_categories = $('table.rubric tr').length
@render_view()
score_lst = []
# get the score for each one
for i in [0..(num_categories-1)]
score = $("input[name='score-selection-#{i}']:checked").val()
score_lst.push(score)
return score_lst
ajax_callback: (response) => ajax_callback: (response) =>
# always clear out errors and messages on transition. # always clear out errors and messages on transition.
...@@ -231,7 +281,8 @@ class StaffGrading ...@@ -231,7 +281,8 @@ class StaffGrading
skip_and_get_next: () => skip_and_get_next: () =>
data = data =
score: @score score: @grade
rubric_scores: @get_score_list()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_id: @submission_id submission_id: @submission_id
location: @location location: @location
...@@ -244,7 +295,8 @@ class StaffGrading ...@@ -244,7 +295,8 @@ class StaffGrading
submit_and_get_next: () -> submit_and_get_next: () ->
data = data =
score: @score score: @grade
rubric_scores: @get_score_list()
feedback: @feedback_area.val() feedback: @feedback_area.val()
submission_id: @submission_id submission_id: @submission_id
location: @location location: @location
...@@ -261,8 +313,8 @@ class StaffGrading ...@@ -261,8 +313,8 @@ class StaffGrading
@rubric = response.rubric @rubric = response.rubric
@submission_id = response.submission_id @submission_id = response.submission_id
@feedback_area.val('') @feedback_area.val('')
@grade = null
@max_score = response.max_score @max_score = response.max_score
@score = null
@ml_error_info=response.ml_error_info @ml_error_info=response.ml_error_info
@prompt_name = response.problem_name @prompt_name = response.problem_name
@num_graded = response.num_graded @num_graded = response.num_graded
...@@ -282,14 +334,21 @@ class StaffGrading ...@@ -282,14 +334,21 @@ class StaffGrading
@ml_error_info = null @ml_error_info = null
@submission_id = null @submission_id = null
@message = message @message = message
@score = null @grade = null
@max_score = 0 @max_score = 0
@state = state_no_data @state = state_no_data
render_view: () -> render_view: () ->
# clear the problem list and breadcrumbs # clear the problem list and breadcrumbs
@problem_list.html('') @problem_list.html('''
<tr>
<th>Problem Name</th>
<th>Graded</th>
<th>Available to Grade</th>
<th>Required</th>
<th>Progress</th>
</tr>
''')
@breadcrumbs.html('') @breadcrumbs.html('')
@problem_list_container.toggle(@list_view) @problem_list_container.toggle(@list_view)
if @backend.mock_backend if @backend.mock_backend
...@@ -306,7 +365,6 @@ class StaffGrading ...@@ -306,7 +365,6 @@ class StaffGrading
@state == state_no_data) @state == state_no_data)
@prompt_wrapper.toggle(show_grading_elements) @prompt_wrapper.toggle(show_grading_elements)
@submission_wrapper.toggle(show_grading_elements) @submission_wrapper.toggle(show_grading_elements)
@rubric_wrapper.toggle(show_grading_elements)
@grading_wrapper.toggle(show_grading_elements) @grading_wrapper.toggle(show_grading_elements)
@meta_info_wrapper.toggle(show_grading_elements) @meta_info_wrapper.toggle(show_grading_elements)
@action_button.hide() @action_button.hide()
...@@ -318,7 +376,7 @@ class StaffGrading ...@@ -318,7 +376,7 @@ class StaffGrading
problem_link:(problem) -> problem_link:(problem) ->
link = $('<a>').attr('href', "javascript:void(0)").append( link = $('<a>').attr('href', "javascript:void(0)").append(
"#{problem.problem_name} (#{problem.num_graded} graded, #{problem.num_pending} pending, required to grade #{problem.num_required} more)") "#{problem.problem_name}")
.click => .click =>
@get_next_submission problem.location @get_next_submission problem.location
...@@ -331,7 +389,17 @@ class StaffGrading ...@@ -331,7 +389,17 @@ class StaffGrading
render_list: () -> render_list: () ->
for problem in @problems for problem in @problems
@problem_list.append($('<li>').append(@problem_link(problem))) problem_row = $('<tr>')
problem_row.append($('<td class="problem-name">').append(@problem_link(problem)))
problem_row.append($('<td>').append("#{problem.num_graded}"))
problem_row.append($('<td>').append("#{problem.num_pending}"))
problem_row.append($('<td>').append("#{problem.num_required}"))
row_progress_bar = $('<div>').addClass('progress-bar')
progress_value = parseInt(problem.num_graded)
progress_max = parseInt(problem.num_required) + progress_value
row_progress_bar.progressbar({value: progress_value, max: progress_max})
problem_row.append($('<td>').append(row_progress_bar))
@problem_list.append(problem_row)
render_problem: () -> render_problem: () ->
# make the view elements match the state. Idempotent. # make the view elements match the state. Idempotent.
...@@ -353,7 +421,7 @@ class StaffGrading ...@@ -353,7 +421,7 @@ class StaffGrading
else if @state == state_grading else if @state == state_grading
@ml_error_info_container.html(@ml_error_info) @ml_error_info_container.html(@ml_error_info)
meta_list = $("<ul>") meta_list = $("<ul>")
meta_list.append("<li><span class='meta-info'>Pending - </span> #{@num_pending}</li>") meta_list.append("<li><span class='meta-info'>Available - </span> #{@num_pending}</li>")
meta_list.append("<li><span class='meta-info'>Graded - </span> #{@num_graded}</li>") meta_list.append("<li><span class='meta-info'>Graded - </span> #{@num_graded}</li>")
meta_list.append("<li><span class='meta-info'>Needed for ML - </span> #{Math.max(@min_for_ml - @num_graded, 0)}</li>") meta_list.append("<li><span class='meta-info'>Needed for ML - </span> #{Math.max(@min_for_ml - @num_graded, 0)}</li>")
@problem_meta_info.html(meta_list) @problem_meta_info.html(meta_list)
...@@ -361,8 +429,6 @@ class StaffGrading ...@@ -361,8 +429,6 @@ class StaffGrading
@prompt_container.html(@prompt) @prompt_container.html(@prompt)
@prompt_name_container.html("#{@prompt_name}") @prompt_name_container.html("#{@prompt_name}")
@submission_container.html(@make_paragraphs(@submission)) @submission_container.html(@make_paragraphs(@submission))
@rubric_container.html(@rubric)
# no submit button until user picks grade. # no submit button until user picks grade.
show_submit_button = false show_submit_button = false
show_action_button = false show_action_button = false
......
...@@ -24,15 +24,33 @@ div.peer-grading{ ...@@ -24,15 +24,33 @@ div.peer-grading{
color: white; color: white;
} }
input[name='score-selection'] { input[name='score-selection'],
input[name='grade-selection'] {
display: none; display: none;
} }
ul .problem-list
{ {
li text-align: center;
table-layout: auto;
width:100%;
th
{
padding: 10px;
}
td
{
padding:10px;
}
td.problem-name
{
text-align:left;
}
.ui-progressbar
{ {
margin: 16px 0px; height:1em;
margin:0px;
padding:0px;
} }
} }
...@@ -106,6 +124,7 @@ div.peer-grading{ ...@@ -106,6 +124,7 @@ div.peer-grading{
margin: 0px; margin: 0px;
background: #eee; background: #eee;
height: 10em; height: 10em;
width:47.6%;
h3 h3
{ {
text-align:center; text-align:center;
...@@ -120,12 +139,10 @@ div.peer-grading{ ...@@ -120,12 +139,10 @@ div.peer-grading{
.calibration-panel .calibration-panel
{ {
float:left; float:left;
width:48%;
} }
.grading-panel .grading-panel
{ {
float:right; float:right;
width: 48%;
} }
.current-state .current-state
{ {
...@@ -159,5 +176,49 @@ div.peer-grading{ ...@@ -159,5 +176,49 @@ div.peer-grading{
} }
} }
padding: 40px; padding: 40px;
.rubric {
tr {
margin:10px 0px;
height: 100%;
}
td {
padding: 20px 0px 25px 0px;
height: 100%;
}
th {
padding: 5px;
margin: 5px;
}
label,
.view-only {
margin:2px;
position: relative;
padding: 15px 15px 25px 15px;
width: 150px;
height:100%;
display: inline-block;
min-height: 50px;
min-width: 50px;
background-color: #CCC;
font-size: .9em;
}
.grade {
position: absolute;
bottom:0px;
right:0px;
margin:10px;
}
.selected-grade {
background: #666;
color: white;
}
input[type=radio]:checked + label {
background: #666;
color: white; }
input[class='score-selection'] {
display: none;
}
}
} }
<section id="combined-open-ended" class="combined-open-ended" data-ajax-url="${ajax_url}" data-allow_reset="${allow_reset}" data-state="${state}" data-task-count="${task_count}" data-task-number="${task_number}"> <section id="combined-open-ended" class="combined-open-ended" data-ajax-url="${ajax_url}" data-allow_reset="${allow_reset}" data-state="${state}" data-task-count="${task_count}" data-task-number="${task_number}">
<h2>${display_name}</h2>
<div class="status-container"> <div class="status-container">
<h4>Status</h4><br/>
${status | n} ${status | n}
</div> </div>
<div class="item-container"> <div class="item-container">
<h4>Problem</h4><br/> <h4>Problem</h4>
<div class="problem-container">
% for item in items: % for item in items:
<div class="item">${item['content'] | n}</div> <div class="item">${item['content'] | n}</div>
% endfor % endfor
</div>
<input type="button" value="Reset" class="reset-button" name="reset"/> <input type="button" value="Reset" class="reset-button" name="reset"/>
<input type="button" value="Next Step" class="next-step-button" name="reset"/> <input type="button" value="Next Step" class="next-step-button" name="reset"/>
</div> </div>
<a name="results" />
<div class="result-container"> <div class="result-container">
</div> </div>
</section> </section>
......
<div class="result-container"> <div class="result-container">
<h4>Results from Step ${task_number}</h4><br/> <h4>Results from Step ${task_number}</h4>
${results | n} ${results | n}
</div> </div>
%if status_list[0]['state'] != 'initial':
<h4>Status</h4>
<div class="status-elements">
<section id="combined-open-ended-status" class="combined-open-ended-status"> <section id="combined-open-ended-status" class="combined-open-ended-status">
%for i in xrange(0,len(status_list)): %for i in xrange(0,len(status_list)):
<%status=status_list[i]%> <%status=status_list[i]%>
%if i==len(status_list)-1: %if i==len(status_list)-1:
<div class="statusitem-current" data-status-number="${i}"> <div class="statusitem statusitem-current" data-status-number="${i}">
%else: %else:
<div class="statusitem" data-status-number="${i}"> <div class="statusitem" data-status-number="${i}">
%endif %endif
...@@ -20,9 +23,12 @@ ...@@ -20,9 +23,12 @@
%if status['type']=="openended" and status['state'] in ['done', 'post_assessment']: %if status['type']=="openended" and status['state'] in ['done', 'post_assessment']:
<div class="show-results"> <div class="show-results">
<a href="#" class="show-results-button">Show results from step ${status['task_number']}</a> <a href="#results" class="show-results-button">Show results from Step ${status['task_number']}</a>
</div> </div>
%endif %endif
</div> </div>
%endfor %endfor
</section> </section>
</div>
%endif
...@@ -273,12 +273,16 @@ ...@@ -273,12 +273,16 @@
% if cert_status['status'] == 'processing': % if cert_status['status'] == 'processing':
<p class="message-copy">Final course details are being wrapped up at <p class="message-copy">Final course details are being wrapped up at
this time. Your final standing will be available shortly.</p> this time. Your final standing will be available shortly.</p>
% elif cert_status['status'] in ('generating', 'ready', 'notpassing'): % elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
<p class="message-copy">Your final grade: <p class="message-copy">Your final grade:
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>. <span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% if cert_status['status'] == 'notpassing': % if cert_status['status'] == 'notpassing':
Grade required for a certificate: <span class="grade-value"> Grade required for a certificate: <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>. ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% elif cert_status['status'] == 'restricted':
<p class="message-copy">
Your certificate is being held pending confirmation that the issuance of your certificate is in compliance with strict U.S. embargoes on Iran, Cuba, Syria and Sudan. If you think our system has mistakenly identified you as being connected with one of those countries, please let us know by contacting <a class="contact-link" href="mailto:info@edx.org">info@edx.org</a>.
</p>
% endif % endif
</p> </p>
% endif % endif
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
</div> </div>
<h2>Problem List</h2> <h2>Problem List</h2>
<ul class="problem-list"> <table class="problem-list">
</ul> </table>
</section> </section>
<!-- Grading View --> <!-- Grading View -->
...@@ -54,11 +54,6 @@ ...@@ -54,11 +54,6 @@
<div class="prompt-container"> <div class="prompt-container">
</div> </div>
</div> </div>
<div class="rubric-wrapper">
<h3>Grading Rubric</h3>
<div class="rubric-container">
</div>
</div>
</section> </section>
...@@ -78,6 +73,8 @@ ...@@ -78,6 +73,8 @@
<div class="evaluation"> <div class="evaluation">
<p class="score-selection-container"> <p class="score-selection-container">
</p> </p>
<p class="grade-selection-container">
</p>
<textarea name="feedback" placeholder="Feedback for student (optional)" <textarea name="feedback" placeholder="Feedback for student (optional)"
class="feedback-area" cols="70" ></textarea> class="feedback-area" cols="70" ></textarea>
</div> </div>
......
...@@ -10,11 +10,11 @@ ...@@ -10,11 +10,11 @@
% if state == 'initial': % if state == 'initial':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span> <span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif state in ['done', 'post_assessment'] and correct == 'correct': % elif state in ['done', 'post_assessment'] and correct == 'correct':
<span class="correct" id="status_${id}">Correct</span> <span class="correct" id="status_${id}"></span> <p>Correct</p>
% elif state in ['done', 'post_assessment'] and correct == 'incorrect': % elif state in ['done', 'post_assessment'] and correct == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span> <span class="incorrect" id="status_${id}"></span> <p>Incorrect. </p>
% elif state == 'assessing': % elif state == 'assessing':
<span class="grading" id="status_${id}">Submitted for grading</span> <span class="grading" id="status_${id}">Submitted for grading.</span>
% endif % endif
% if hidden: % if hidden:
......
<section> <section>
<header>Feedback</header> <header>Feedback</header>
<div class="shortform"> <div class="shortform-custom" data-open-text='Show detailed results' data-close-text='Hide detailed results'>
<div class="result-output"> <div class="result-output">
<p>Score: ${score}</p> <p>Score: ${score}</p>
% if grader_type == "ML": % if grader_type == "ML":
......
<table class="rubric"> <form class="rubric-template" id="inputtype_${id}">
% for i in range(len(rubric_categories)): <h3>Rubric</h3>
<% category = rubric_categories[i] %> % if view_only and has_score:
<tr> <p>This is the rubric that was used to grade your submission. The highlighted selection matches how the grader feels you performed in each category.</p>
<th> % elif view_only:
${category['description']} <p>Use the below rubric to rate this submission.</p>
% if category['has_score'] == True: % else:
(Your score: ${category['score']}) <p>Select the criteria you feel best represents this submission in each category.</p>
% endif % endif
</th> <table class="rubric">
% for i in range(len(categories)):
<% category = categories[i] %>
<tr>
<th>${category['description']}</th>
% for j in range(len(category['options'])): % for j in range(len(category['options'])):
<% option = category['options'][j] %> <% option = category['options'][j] %>
<td> <td>
% if view_only:
## if this is the selected rubric block, show it highlighted
% if option['selected']:
<div class="view-only selected-grade">
% else:
<div class="view-only"> <div class="view-only">
${option['text']}
% if option.has_key('selected'):
% if option['selected'] == True:
<div class="selected-grade">[${option['points']} points]</div>
%else:
<div class="grade">[${option['points']} points]</div>
% endif % endif
% else: ${option['text']}
<div class="grade">[${option['points']} points]</div> <div class="grade">[${option['points']} points]</div>
%endif
</div> </div>
% else:
<input type="radio" class="score-selection" name="score-selection-${i}" id="score-${i}-${j}" value="${option['points']}"/>
<label for="score-${i}-${j}">${option['text']}</label>
% endif
</td> </td>
% endfor % endfor
</tr> </tr>
% endfor % endfor
</table> </table>
\ No newline at end of file </form>
...@@ -26,13 +26,37 @@ ...@@ -26,13 +26,37 @@
Nothing to grade! Nothing to grade!
</div> </div>
%else: %else:
<ul class="problem-list"> <div class="problem-list-container">
<table class="problem-list">
<tr>
<th>Problem Name</th>
<th>Graded</th>
<th>Available</th>
<th>Required</th>
<th>Progress</th>
</tr>
%for problem in problem_list: %for problem in problem_list:
<li> <tr data-graded="${problem['num_graded']}" data-required="${problem['num_required']}">
<a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']} (${problem['num_graded']} graded, ${problem['num_pending']} pending, required to grade ${problem['num_required']} more)</a> <td class="problem-name">
</li> <a href="${ajax_url}problem?location=${problem['location']}">${problem['problem_name']}</a>
</td>
<td>
${problem['num_graded']}
</td>
<td>
${problem['num_pending']}
</td>
<td>
${problem['num_required']}
</td>
<td>
<div class="progress-bar">
</div>
</td>
</tr>
%endfor %endfor
</ul> </table>
</div>
%endif %endif
%endif %endif
</div> </div>
......
...@@ -44,20 +44,13 @@ ...@@ -44,20 +44,13 @@
</div> </div>
<div class="prompt-wrapper"> <div class="prompt-wrapper">
<div class="prompt-information-container collapsible"> <h2>Question</h2>
<header><a href="javascript:void(0)">Question</a></header> <div class="prompt-information-container">
<section> <section>
<div class="prompt-container"> <div class="prompt-container">
</div> </div>
</section> </section>
</div> </div>
<div class="rubric-wrapper collapsible">
<header><a href="javascript:void(0)">Rubric</a></header>
<section>
<div class="rubric-container">
</div>
</section>
</div>
</div> </div>
...@@ -74,6 +67,7 @@ ...@@ -74,6 +67,7 @@
<input type="hidden" name="essay-id" value="" /> <input type="hidden" name="essay-id" value="" />
</div> </div>
<div class="evaluation"> <div class="evaluation">
<p class="rubric-selection-container"></p>
<p class="score-selection-container"> <p class="score-selection-container">
</p> </p>
<textarea name="feedback" placeholder="Feedback for student (optional)" <textarea name="feedback" placeholder="Feedback for student (optional)"
......
<div class="assessment"> <div class="assessment-container">
<div class="rubric"> <div class="rubric">
<h3>Self-assess your answer with this rubric:</h3>
${rubric | n } ${rubric | n }
</div> </div>
% if not read_only: % if not read_only:
<select name="assessment" class="assessment"> <div class="scoring-container">
<h3>Scoring</h3>
<p>Please select a score below:</p>
<div class="grade-selection">
%for i in xrange(0,max_score+1): %for i in xrange(0,max_score+1):
<option value="${i}">${i}</option> <% id = "score-{0}".format(i) %>
<input type="radio" class="grade-selection" name="grade-selection" value="${i}" id="${id}">
<label for="${id}">${i}</label>
%endfor %endfor
</select> </div>
</div>
% endif % endif
</div> </div>
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