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/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 838680c844..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 @@ -409,11 +410,11 @@ class TestCenterRegistration(models.Model): # 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) @@ -574,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/lms/djangoapps/certificates/management/commands/cert_whitelist.py b/lms/djangoapps/certificates/management/commands/cert_whitelist.py new file mode 100644 index 0000000000..94dfda74a6 --- /dev/null +++ b/lms/djangoapps/certificates/management/commands/cert_whitelist.py @@ -0,0 +1,74 @@ +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])) diff --git a/lms/djangoapps/certificates/migrations/0014_adding_whitelist.py b/lms/djangoapps/certificates/migrations/0014_adding_whitelist.py new file mode 100644 index 0000000000..1bd4d994cf --- /dev/null +++ b/lms/djangoapps/certificates/migrations/0014_adding_whitelist.py @@ -0,0 +1,89 @@ +# -*- 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 diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index b9bd55b9af..0e68e3cfe7 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -35,6 +35,19 @@ State diagram: v v v [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): deleted = 'deleted' downloadable = 'downloadable' notpassing = 'notpassing' + restricted = 'restricted' 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): user = models.ForeignKey(User) @@ -87,6 +112,10 @@ def certificate_status_for_student(student, course_id): deleted - The certificate has been deleted. downloadable - The certificate is available for download. 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 "download_url". diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py index b9316220fa..d926035efd 100644 --- a/lms/djangoapps/certificates/queue.py +++ b/lms/djangoapps/certificates/queue.py @@ -1,6 +1,7 @@ from certificates.models import GeneratedCertificate from certificates.models import certificate_status_for_student from certificates.models import CertificateStatuses as status +from certificates.models import CertificateWhitelist from courseware import grades, courses from django.test.client import RequestFactory @@ -71,6 +72,8 @@ class XQueueCertInterface(object): settings.XQUEUE_INTERFACE['django_auth'], requests_auth, ) + self.whitelist = CertificateWhitelist.objects.all() + self.restricted = UserProfile.objects.filter(allow_certificate=False) def regen_cert(self, student, course_id): """ @@ -93,49 +96,7 @@ class XQueueCertInterface(object): """ - VALID_STATUSES = [status.error, status.downloadable] - - 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 + raise NotImplementedError def del_cert(self, student, course_id): @@ -152,34 +113,7 @@ class XQueueCertInterface(object): """ - VALID_STATUSES = [status.error, status.downloadable] - - 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 + raise NotImplementedError def add_cert(self, student, course_id): """ @@ -189,13 +123,17 @@ class XQueueCertInterface(object): course_id - courseenrollment.course_id (string) 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', 'deleted' or 'generating' state. - If a student has a passing grade a request will made - for a new cert + If a student has a passing grade or is in the whitelist + 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 will change to status.notpassing @@ -214,30 +152,41 @@ class XQueueCertInterface(object): 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) + + cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) - if grade['grade'] is not None: - cert_status = status.generating + grade = grades.grade(student, self.request, course) + 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()) - cert.status = cert_status cert.grade = grade['percent'] cert.user = student cert.course_id = course_id cert.key = key cert.name = profile.name - contents = { - 'action': 'create', - 'username': student.username, - 'course_id': course_id, - 'name': profile.name, - } - - self._send_to_xqueue(contents, key) + # 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 = { + 'action': 'create', + 'username': student.username, + 'course_id': course_id, + 'name': profile.name, + } + cert.status = status.generating + self._send_to_xqueue(contents, key) cert.save() else: cert_status = status.notpassing diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index 8ec58a6a28..43b69ea44b 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -273,12 +273,16 @@ % if cert_status['status'] == 'processing':
- % elif cert_status['status'] in ('generating', 'ready', 'notpassing'): + % elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'): % endif % endif