Merge pull request #1369 from MITx/feature/jarv/certificate-restriction
Feature/jarv/certificate restriction
This commit is contained in:
@@ -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()
|
||||
@@ -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']
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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]))
|
||||
@@ -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']
|
||||
@@ -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".
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -273,12 +273,16 @@
|
||||
% if cert_status['status'] == 'processing':
|
||||
<p class="message-copy">Final course details are being wrapped up at
|
||||
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:
|
||||
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
|
||||
% if cert_status['status'] == 'notpassing':
|
||||
Grade required for a certificate: <span class="grade-value">
|
||||
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
|
||||
% elif cert_status['status'] == 'restricted':
|
||||
<p class="message-copy">
|
||||
Your certificate is being held while we seek 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
|
||||
</p>
|
||||
% endif
|
||||
|
||||
Reference in New Issue
Block a user