diff --git a/cms/static/coffee/files.json b/cms/static/coffee/files.json index b396bec944..ec596063a9 100644 --- a/cms/static/coffee/files.json +++ b/cms/static/coffee/files.json @@ -3,6 +3,7 @@ "/static/js/vendor/jquery.min.js", "/static/js/vendor/json2.js", "/static/js/vendor/underscore-min.js", - "/static/js/vendor/backbone-min.js" + "/static/js/vendor/backbone-min.js", + "/static/js/vendor/RequireJS.js" ] } diff --git a/cms/static/js/base.js b/cms/static/js/base.js index ee8df97b28..41c1ee3cdb 100644 --- a/cms/static/js/base.js +++ b/cms/static/js/base.js @@ -87,7 +87,10 @@ $(document).ready(function() { $('.unit').draggable({ axis: 'y', handle: '.drag-handle', - stack: '.unit', + zIndex: 999, + start: initiateHesitate, + drag: checkHoverState, + stop: removeHesitate, revert: "invalid" }); @@ -95,7 +98,10 @@ $(document).ready(function() { $('.id-holder').draggable({ axis: 'y', handle: '.section-item .drag-handle', - stack: '.id-holder', + zIndex: 999, + start: initiateHesitate, + drag: checkHoverState, + stop: removeHesitate, revert: "invalid" }); @@ -179,10 +185,12 @@ function toggleSections(e) { if($button.hasClass('is-activated')) { $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 { $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) { 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) { - $(event.delegateTarget).removeClass('collapsed'); - $(event.delegateTarget).find('.expand-collapse-icon').removeClass('expand').addClass('collapse'); + $(event.delegateTarget).removeClass('collapsed', 400); + // 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) { diff --git a/cms/static/js/hesitate.js b/cms/static/js/hesitate.js index 63806ba0ec..c5848a6c0c 100644 --- a/cms/static/js/hesitate.js +++ b/cms/static/js/hesitate.js @@ -18,33 +18,31 @@ CMS.HesitateEvent = function(executeOnTimeOut, cancelSelector, onlyOnce) { this.timeoutEventId = null; this.originalEvent = null; this.onlyOnce = (onlyOnce === true); -} +}; -CMS.HesitateEvent.DURATION = 400; +CMS.HesitateEvent.DURATION = 800; CMS.HesitateEvent.prototype.trigger = function(event) { -console.log('trigger'); - if (this.timeoutEventId === null) { - this.timeoutEventId = window.setTimeout(this.fireEvent, CMS.HesitateEvent.DURATION); - this.originalEvent = event; - // is it wrong to bind to the below v $(event.currentTarget)? - $(this.originalEvent.delegateTarget).on(this.cancelSelector, this.untrigger); + if (event.data.timeoutEventId == null) { + event.data.timeoutEventId = window.setTimeout( + function() { event.data.fireEvent(event); }, + CMS.HesitateEvent.DURATION); + event.data.originalEvent = event; + $(event.data.originalEvent.delegateTarget).on(event.data.cancelSelector, event.data, event.data.untrigger); } -} +}; CMS.HesitateEvent.prototype.fireEvent = function(event) { -console.log('fire'); - this.timeoutEventId = null; - $(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger); - if (this.onlyOnce) $(this.originalEvent.delegateTarget).off(this.originalEvent.type, this.trigger); - this.executeOnTimeOut(this.originalEvent); -} + event.data.timeoutEventId = null; + $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger); + if (event.data.onlyOnce) $(event.data.originalEvent.delegateTarget).off(event.data.originalEvent.type, event.data.trigger); + event.data.executeOnTimeOut(event.data.originalEvent); +}; CMS.HesitateEvent.prototype.untrigger = function(event) { -console.log('untrigger'); - if (this.timeoutEventId) { - window.clearTimeout(this.timeoutEventId); - $(this.originalEvent.delegateTarget).off(this.cancelSelector, this.untrigger); + if (event.data.timeoutEventId) { + window.clearTimeout(event.data.timeoutEventId); + $(event.data.originalEvent.delegateTarget).off(event.data.cancelSelector, event.data.untrigger); } - this.timeoutEventId = null; -} \ No newline at end of file + event.data.timeoutEventId = null; +}; \ No newline at end of file diff --git a/common/djangoapps/course_groups/tests/tests.py b/common/djangoapps/course_groups/tests/tests.py index 21fad8bbeb..86f0be0791 100644 --- a/common/djangoapps/course_groups/tests/tests.py +++ b/common/djangoapps/course_groups/tests/tests.py @@ -10,6 +10,25 @@ from course_groups.cohorts import (get_cohort, get_course_cohorts, from xmodule.modulestore.django import modulestore, _MODULESTORES +# NOTE: running this with the lms.envs.test config works without +# manually overriding the modulestore. However, running with +# cms.envs.test doesn't. + +def xml_store_config(data_dir): + return { + 'default': { + 'ENGINE': 'xmodule.modulestore.xml.XMLModuleStore', + 'OPTIONS': { + 'data_dir': data_dir, + 'default_class': 'xmodule.hidden_module.HiddenDescriptor', + } + } +} + +TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT +TEST_DATA_XML_MODULESTORE = xml_store_config(TEST_DATA_DIR) + +@override_settings(MODULESTORE=TEST_DATA_XML_MODULESTORE) class TestCohorts(django.test.TestCase): @@ -77,7 +96,7 @@ class TestCohorts(django.test.TestCase): course = modulestore().get_course("edX/toy/2012_Fall") self.assertEqual(course.id, "edX/toy/2012_Fall") self.assertFalse(course.is_cohorted) - + user = User.objects.create(username="test", email="a@b.com") other_user = User.objects.create(username="test2", email="a2@b.com") diff --git a/common/djangoapps/student/management/commands/cert_restriction.py b/common/djangoapps/student/management/commands/cert_restriction.py new file mode 100644 index 0000000000..c43ff05f3e --- /dev/null +++ b/common/djangoapps/student/management/commands/cert_restriction.py @@ -0,0 +1,109 @@ +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() diff --git a/common/djangoapps/student/management/commands/pearson_dump.py b/common/djangoapps/student/management/commands/pearson_dump.py new file mode 100644 index 0000000000..28931dc468 --- /dev/null +++ b/common/djangoapps/student/management/commands/pearson_dump.py @@ -0,0 +1,76 @@ +from optparse import make_option +from json import dump + +from django.core.management.base import BaseCommand, CommandError + +from student.models import TestCenterRegistration + + +class Command(BaseCommand): + + args = '' + help = """ + Dump information as JSON from TestCenterRegistration tables, including username and status. + """ + + option_list = BaseCommand.option_list + ( + make_option('--course_id', + action='store', + dest='course_id', + help='Specify a particular course.'), + make_option('--exam_series_code', + action='store', + dest='exam_series_code', + default=None, + help='Specify a particular exam, using the Pearson code'), + make_option('--accommodation_pending', + action='store_true', + dest='accommodation_pending', + default=False, + ), + ) + + def handle(self, *args, **options): + if len(args) < 1: + raise CommandError("Missing single argument: output JSON file") + + # get output location: + outputfile = args[0] + + # construct the query object to dump: + registrations = TestCenterRegistration.objects.all() + if 'course_id' in options and options['course_id']: + registrations = registrations.filter(course_id=options['course_id']) + if 'exam_series_code' in options and options['exam_series_code']: + registrations = registrations.filter(exam_series_code=options['exam_series_code']) + + # collect output: + output = [] + for registration in registrations: + if 'accommodation_pending' in options and options['accommodation_pending'] and not registration.accommodation_is_pending: + continue + record = {'username' : registration.testcenter_user.user.username, + 'email' : registration.testcenter_user.email, + 'first_name' : registration.testcenter_user.first_name, + 'last_name' : registration.testcenter_user.last_name, + 'client_candidate_id' : registration.client_candidate_id, + 'client_authorization_id' : registration.client_authorization_id, + 'course_id' : registration.course_id, + 'exam_series_code' : registration.exam_series_code, + 'accommodation_request' : registration.accommodation_request, + 'accommodation_code' : registration.accommodation_code, + 'registration_status' : registration.registration_status(), + 'demographics_status' : registration.demographics_status(), + 'accommodation_status' : registration.accommodation_status(), + } + if len(registration.upload_error_message) > 0: + record['registration_error'] = registration.upload_error_message + if registration.needs_uploading: + record['needs_uploading'] = True + + output.append(record) + + # dump output: + with open(outputfile, 'w') as outfile: + dump(output, outfile) + diff --git a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py index 9c3a34a90c..d94c3ba863 100644 --- a/common/djangoapps/student/management/commands/pearson_import_conf_zip.py +++ b/common/djangoapps/student/management/commands/pearson_import_conf_zip.py @@ -65,7 +65,7 @@ class Command(BaseCommand): else: try: registration = TestCenterRegistration.objects.get(client_authorization_id=client_authorization_id) - Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile) + Command.datadog_error("Found authorization record for user {}".format(registration.testcenter_user.user.username), eacfile.name) # now update the record: registration.upload_status = row['Status'] registration.upload_error_message = row['Message'] diff --git a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py index 2fcfa9ae48..b59241240d 100644 --- a/common/djangoapps/student/management/commands/pearson_make_tc_registration.py +++ b/common/djangoapps/student/management/commands/pearson_make_tc_registration.py @@ -179,7 +179,8 @@ class Command(BaseCommand): if (len(form.errors) > 0): print "Field Form errors encountered:" for fielderror in form.errors: - print "Field Form Error: %s" % fielderror + for msg in form.errors[fielderror]: + print "Field Form Error: {} -- {}".format(fielderror, msg) if (len(form.non_field_errors()) > 0): print "Non-field Form errors encountered:" for nonfielderror in form.non_field_errors: diff --git a/common/djangoapps/student/migrations/0024_add_allow_certificate.py b/common/djangoapps/student/migrations/0024_add_allow_certificate.py new file mode 100644 index 0000000000..fb3a97cd4b --- /dev/null +++ b/common/djangoapps/student/migrations/0024_add_allow_certificate.py @@ -0,0 +1,172 @@ +# -*- 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 diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 44b947c045..71d2177bd4 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -90,6 +90,7 @@ class UserProfile(models.Model): ) mailing_address = models.TextField(blank=True, null=True) goals = models.TextField(blank=True, null=True) + allow_certificate = models.BooleanField(default=1) def get_meta(self): js_str = self.meta @@ -255,9 +256,9 @@ class TestCenterUserForm(ModelForm): def clean_country(self): code = self.cleaned_data['country'] - if code and len(code) != 3: + if code and (len(code) != 3 or not code.isalpha()): raise forms.ValidationError(u'Must be three characters (ISO 3166-1): e.g. USA, CAN, MNG') - return code + return code.upper() def clean(self): def _can_encode_as_latin(fieldvalue): @@ -387,6 +388,12 @@ class TestCenterRegistration(models.Model): return 'Update' elif self.uploaded_at is None: return 'Add' + elif self.registration_is_rejected: + # Assume that if the registration was rejected before, + # it is more likely this is the (first) correction + # than a second correction in flight before the first was + # processed. + return 'Add' else: # TODO: decide what to send when we have uploaded an initial version, # but have not received confirmation back from that upload. If the @@ -400,13 +407,14 @@ class TestCenterRegistration(models.Model): @property def exam_authorization_count(self): - # TODO: figure out if this should really go in the database (with a default value). + # Someday this could go in the database (with a default value). But at present, + # we do not expect anyone to be authorized to take an exam more than once. return 1 - + @property def needs_uploading(self): return self.uploaded_at is None or self.uploaded_at < self.user_updated_at - + @classmethod def create(cls, testcenter_user, exam, accommodation_request): registration = cls(testcenter_user = testcenter_user) @@ -499,6 +507,33 @@ class TestCenterRegistration(models.Model): def registration_signup_url(self): return settings.PEARSONVUE_SIGNINPAGE_URL + def demographics_status(self): + if self.demographics_is_accepted: + return "Accepted" + elif self.demographics_is_rejected: + return "Rejected" + else: + return "Pending" + + def accommodation_status(self): + if self.accommodation_is_skipped: + return "Skipped" + elif self.accommodation_is_accepted: + return "Accepted" + elif self.accommodation_is_rejected: + return "Rejected" + else: + return "Pending" + + def registration_status(self): + if self.registration_is_accepted: + return "Accepted" + elif self.registration_is_rejected: + return "Rejected" + else: + return "Pending" + + class TestCenterRegistrationForm(ModelForm): class Meta: model = TestCenterRegistration @@ -518,7 +553,15 @@ class TestCenterRegistrationForm(ModelForm): registration.save() log.info("Updated registration information for user's test center exam registration: username \"{}\" course \"{}\", examcode \"{}\"".format(registration.testcenter_user.user.username, registration.course_id, registration.exam_series_code)) - # TODO: add validation code for values added to accommodation_code field. + def clean_accommodation_code(self): + code = self.cleaned_data['accommodation_code'] + if code: + code = code.upper() + codes = code.split('*') + for codeval in codes: + if codeval not in ACCOMMODATION_CODE_DICT: + raise forms.ValidationError(u'Invalid accommodation code specified: "{}"'.format(codeval)) + return code @@ -532,7 +575,7 @@ def get_testcenter_registration(user, course_id, exam_series_code): # nosetests thinks that anything with _test_ in the name is a test. # Correct this (https://nose.readthedocs.org/en/latest/finding_tests.html) get_testcenter_registration.__test__ = False - + def unique_id_for_user(user): """ Return a unique id for a user, suitable for inserting into diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 61b49e6022..b583599e97 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -135,7 +135,7 @@ def cert_info(user, course): Get the certificate info needed to render the dashboard section for the given 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 'download_url': url, only present if show_download_url is True 'show_disabled_download_button': bool -- true if state is 'generating' @@ -168,6 +168,7 @@ def _cert_info(user, course, cert_status): CertificateStatuses.regenerating: 'generating', CertificateStatuses.downloadable: 'ready', CertificateStatuses.notpassing: 'notpassing', + CertificateStatuses.restricted: 'restricted', } status = template_state.get(cert_status['status'], default_status) @@ -176,7 +177,7 @@ def _cert_info(user, course, cert_status): 'show_download_url': status == 'ready', '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): d.update({ 'show_survey_button': True, @@ -192,7 +193,7 @@ def _cert_info(user, course, cert_status): else: 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: # 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). diff --git a/common/lib/xmodule/jasmine_test_runner.html.erb b/common/lib/xmodule/jasmine_test_runner.html.erb index 3327ab4aea..fae6c14cbe 100644 --- a/common/lib/xmodule/jasmine_test_runner.html.erb +++ b/common/lib/xmodule/jasmine_test_runner.html.erb @@ -17,6 +17,7 @@ +