Commit 6857dae0 by jarv

Merge pull request #1034 from MITx/feature/rocha/cert

Feature/rocha/cert
parents 6b12cfd7 fc32032b
...@@ -29,6 +29,9 @@ from django_future.csrf import ensure_csrf_cookie, csrf_exempt ...@@ -29,6 +29,9 @@ from django_future.csrf import ensure_csrf_cookie, csrf_exempt
from student.models import (Registration, UserProfile, from student.models import (Registration, UserProfile,
PendingNameChange, PendingEmailChange, PendingNameChange, PendingEmailChange,
CourseEnrollment) CourseEnrollment)
from certificates.models import CertificateStatuses, certificate_status_for_student
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
...@@ -143,11 +146,15 @@ def dashboard(request): ...@@ -143,11 +146,15 @@ def dashboard(request):
show_courseware_links_for = frozenset(course.id for course in courses show_courseware_links_for = frozenset(course.id for course in courses
if has_access(request.user, course, 'load')) if has_access(request.user, course, 'load'))
cert_statuses = [certificate_status_for_student(request.user, course.id) for course in courses]
context = {'courses': courses, context = {'courses': courses,
'message': message, 'message': message,
'staff_access': staff_access, 'staff_access': staff_access,
'errored_courses': errored_courses, 'errored_courses': errored_courses,
'show_courseware_links_for' : show_courseware_links_for} 'show_courseware_links_for' : show_courseware_links_for,
'cert_statuses': cert_statuses,
}
return render_to_response('dashboard.html', context) return render_to_response('dashboard.html', context)
...@@ -206,13 +213,13 @@ def change_enrollment(request): ...@@ -206,13 +213,13 @@ def change_enrollment(request):
return {'success': False, return {'success': False,
'error': 'enrollment in {} not allowed at this time' 'error': 'enrollment in {} not allowed at this time'
.format(course.display_name)} .format(course.display_name)}
org, course_num, run=course_id.split("/") org, course_num, run=course_id.split("/")
statsd.increment("common.student.enrollment", statsd.increment("common.student.enrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id) enrollment, created = CourseEnrollment.objects.get_or_create(user=user, course_id=course.id)
return {'success': True} return {'success': True}
...@@ -220,13 +227,13 @@ def change_enrollment(request): ...@@ -220,13 +227,13 @@ def change_enrollment(request):
try: try:
enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id) enrollment = CourseEnrollment.objects.get(user=user, course_id=course_id)
enrollment.delete() enrollment.delete()
org, course_num, run=course_id.split("/") org, course_num, run=course_id.split("/")
statsd.increment("common.student.unenrollment", statsd.increment("common.student.unenrollment",
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
return {'success': True} return {'success': True}
except CourseEnrollment.DoesNotExist: except CourseEnrollment.DoesNotExist:
return {'success': False, 'error': 'You are not enrolled for this course.'} return {'success': False, 'error': 'You are not enrolled for this course.'}
...@@ -275,13 +282,13 @@ def login_user(request, error=""): ...@@ -275,13 +282,13 @@ def login_user(request, error=""):
log.info("Login success - {0} ({1})".format(username, email)) log.info("Login success - {0} ({1})".format(username, email))
try_change_enrollment(request) try_change_enrollment(request)
statsd.increment("common.student.successful_login") statsd.increment("common.student.successful_login")
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
log.warning("Login failed - Account not active for user {0}, resending activation".format(username)) log.warning("Login failed - Account not active for user {0}, resending activation".format(username))
reactivation_email_for_user(user) reactivation_email_for_user(user)
not_activated_msg = "This account has not been activated. We have " + \ not_activated_msg = "This account has not been activated. We have " + \
"sent another activation message. Please check your " + \ "sent another activation message. Please check your " + \
...@@ -483,9 +490,9 @@ def create_account(request, post_override=None): ...@@ -483,9 +490,9 @@ def create_account(request, post_override=None):
log.debug('bypassing activation email') log.debug('bypassing activation email')
login_user.is_active = True login_user.is_active = True
login_user.save() login_user.save()
statsd.increment("common.student.account_created") statsd.increment("common.student.account_created")
js = {'success': True} js = {'success': True}
return HttpResponse(json.dumps(js), mimetype="application/json") return HttpResponse(json.dumps(js), mimetype="application/json")
...@@ -541,9 +548,9 @@ def password_reset(request): ...@@ -541,9 +548,9 @@ def password_reset(request):
''' Attempts to send a password reset e-mail. ''' ''' Attempts to send a password reset e-mail. '''
if request.method != "POST": if request.method != "POST":
raise Http404 raise Http404
# By default, Django doesn't allow Users with is_active = False to reset their passwords, # By default, Django doesn't allow Users with is_active = False to reset their passwords,
# but this bites people who signed up a long time ago, never activated, and forgot their # but this bites people who signed up a long time ago, never activated, and forgot their
# password. So for their sake, we'll auto-activate a user for whome password_reset is called. # password. So for their sake, we'll auto-activate a user for whome password_reset is called.
try: try:
user = User.objects.get(email=request.POST['email']) user = User.objects.get(email=request.POST['email'])
...@@ -551,7 +558,7 @@ def password_reset(request): ...@@ -551,7 +558,7 @@ def password_reset(request):
user.save() user.save()
except: except:
log.exception("Tried to auto-activate user to enable password reset, but failed.") log.exception("Tried to auto-activate user to enable password reset, but failed.")
form = PasswordResetForm(request.POST) form = PasswordResetForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save(use_https = request.is_secure(), form.save(use_https = request.is_secure(),
...@@ -589,7 +596,7 @@ def reactivation_email_for_user(user): ...@@ -589,7 +596,7 @@ def reactivation_email_for_user(user):
res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL) res = user.email_user(subject, message, settings.DEFAULT_FROM_EMAIL)
return HttpResponse(json.dumps({'success': True})) return HttpResponse(json.dumps({'success': True}))
@ensure_csrf_cookie @ensure_csrf_cookie
def change_email_request(request): def change_email_request(request):
...@@ -764,8 +771,8 @@ def accept_name_change_by_id(id): ...@@ -764,8 +771,8 @@ def accept_name_change_by_id(id):
@ensure_csrf_cookie @ensure_csrf_cookie
def accept_name_change(request): def accept_name_change(request):
''' JSON: Name change process. Course staff clicks 'accept' on a given name change ''' JSON: Name change process. Course staff clicks 'accept' on a given name change
We used this during the prototype but now we simply record name changes instead We used this during the prototype but now we simply record name changes instead
of manually approving them. Still keeping this around in case we want to go of manually approving them. Still keeping this around in case we want to go
back to this approval method. back to this approval method.
......
...@@ -133,7 +133,7 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -133,7 +133,7 @@ class CourseDescriptor(SequenceDescriptor):
Returns True if the current time is after the specified course end date. Returns True if the current time is after the specified course end date.
Returns False if there is no end date specified. Returns False if there is no end date specified.
""" """
if self.end_date is None: if self.end is None:
return False return False
return time.gmtime() > self.end return time.gmtime() > self.end
...@@ -250,6 +250,10 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -250,6 +250,10 @@ class CourseDescriptor(SequenceDescriptor):
displayed_start = self._try_parse_time('advertised_start') or self.start displayed_start = self._try_parse_time('advertised_start') or self.start
return time.strftime("%b %d, %Y", displayed_start) return time.strftime("%b %d, %Y", displayed_start)
@property
def end_date_text(self):
return time.strftime("%b %d, %Y", self.end)
# An extra property is used rather than the wiki_slug/number because # An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows # there are courses that change the number for different runs. This allows
# courses to share the same css_class across runs even if they have # courses to share the same css_class across runs even if they have
...@@ -299,6 +303,16 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -299,6 +303,16 @@ class CourseDescriptor(SequenceDescriptor):
return self.metadata.get('hide_progress_tab') == True return self.metadata.get('hide_progress_tab') == True
@property @property
def end_of_course_survey_url(self):
"""
Pull from policy. Once we have our own survey module set up, can change this to point to an automatically
created survey for each class.
Returns None if no url specified.
"""
return self.metadata.get('end_of_course_survey_url')
@property
def title(self): def title(self):
return self.display_name return self.display_name
......
...@@ -253,8 +253,10 @@ Supported fields at the course level: ...@@ -253,8 +253,10 @@ Supported fields at the course level:
* "advertised_start" -- specify what you want displayed as the start date of the course in the course listing and course about pages. This can be useful if you want to let people in early before the formal start. Format-by-example: "2012-09-05T12:00". * "advertised_start" -- specify what you want displayed as the start date of the course in the course listing and course about pages. This can be useful if you want to let people in early before the formal start. Format-by-example: "2012-09-05T12:00".
* "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start". * "enrollment_start", "enrollment_end" -- when can students enroll? (if not specified, can enroll anytime). Same format as "start".
* "end" -- specify the end date for the course. Format-by-example: "2012-11-05T12:00". * "end" -- specify the end date for the course. Format-by-example: "2012-11-05T12:00".
* "end_of_course_survey_url" -- a url for an end of course survey -- shown after course is over, next to certificate download links.
* "tabs" -- have custom tabs in the courseware. See below for details on config. * "tabs" -- have custom tabs in the courseware. See below for details on config.
* "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]] * "discussion_blackouts" -- An array of time intervals during which you want to disable a student's ability to create or edit posts in the forum. Moderators, Community TAs, and Admins are unaffected. You might use this during exam periods, but please be aware that the forum is often a very good place to catch mistakes and clarify points to students. The better long term solution would be to have better flagging/moderation mechanisms, but this is the hammer we have today. Format by example: [["2012-10-29T04:00", "2012-11-03T04:00"], ["2012-12-30T04:00", "2013-01-02T04:00"]]
* "show_calculator" (value "Yes" if desired)
* TODO: there are others * TODO: there are others
### Grading policy file contents ### Grading policy file contents
......
# -*- coding: utf-8 -*-
from django.core.management.base import BaseCommand
from certificates.models import certificate_status_for_student
from certificates.queue import XQueueCertInterface
from django.contrib.auth.models import User
from student.models import UserProfile
class Command(BaseCommand):
help = """
Looks for names that have unicode characters
and queues them up for a certificate request
"""
def handle(self, *args, **options):
# TODO this is only temporary for CS169 certs
course_id = 'BerkeleyX/CS169.1x/2012_Fall'
enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related(
"groups").order_by('username')
xq = XQueueCertInterface()
print "Looking for unusual names.."
for student in enrolled_students:
if certificate_status_for_student(
student, course_id)['status'] == 'unavailable':
continue
name = UserProfile.objects.get(user=student).name
for c in name:
if ord(c) >= 0x200:
ret = xq.add_cert(student, course_id)
if ret == 'generating':
print 'generating for {0}'.format(student)
break
from django.utils.simplejson import dumps from django.core.management.base import BaseCommand
from django.core.management.base import BaseCommand, CommandError from certificates.models import certificate_status_for_student
from certificates.models import GeneratedCertificate from certificates.queue import XQueueCertInterface
from django.contrib.auth.models import User
class Command(BaseCommand): class Command(BaseCommand):
help = """ help = """
This command finds all GeneratedCertificate objects that do not have a Find all students that have need certificates
certificate generated. These come into being when a user requests a and put certificate requests on the queue
certificate, or when grade_all_students is called (for pre-generating
certificates).
It returns a json formatted list of users and their user ids This is only for BerkeleyX/CS169.1x/2012_Fall
""" """
def handle(self, *args, **options): def handle(self, *args, **options):
users = GeneratedCertificate.objects.filter(
download_url=None) # TODO This is only temporary for CS169 certs
user_output = [{'user_id':user.user_id, 'name':user.name}
for user in users] course_id = 'BerkeleyX/CS169.1x/2012_Fall'
self.stdout.write(dumps(user_output) + "\n") enrolled_students = User.objects.filter(
courseenrollment__course_id=course_id).prefetch_related(
"groups").order_by('username')
xq = XQueueCertInterface()
for student in enrolled_students:
if certificate_status_for_student(
student, course_id)['status'] == 'unavailable':
ret = xq.add_cert(student, course_id)
if ret == 'generating':
print 'generating for {0}'.format(student)
# -*- 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):
# Deleting field 'GeneratedCertificate.graded_download_url'
db.delete_column('certificates_generatedcertificate', 'graded_download_url')
# Deleting field 'GeneratedCertificate.graded_certificate_id'
db.delete_column('certificates_generatedcertificate', 'graded_certificate_id')
# Adding field 'GeneratedCertificate.distinction'
db.add_column('certificates_generatedcertificate', 'distinction',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding unique constraint on 'GeneratedCertificate', fields ['course_id', 'user']
db.create_unique('certificates_generatedcertificate', ['course_id', 'user_id'])
def backwards(self, orm):
# Removing unique constraint on 'GeneratedCertificate', fields ['course_id', 'user']
db.delete_unique('certificates_generatedcertificate', ['course_id', 'user_id'])
# Adding field 'GeneratedCertificate.graded_download_url'
db.add_column('certificates_generatedcertificate', 'graded_download_url',
self.gf('django.db.models.fields.CharField')(default=False, max_length=128),
keep_default=False)
# Adding field 'GeneratedCertificate.graded_certificate_id'
db.add_column('certificates_generatedcertificate', 'graded_certificate_id',
self.gf('django.db.models.fields.CharField')(default=False, max_length=32),
keep_default=False)
# Deleting field 'GeneratedCertificate.distinction'
db.delete_column('certificates_generatedcertificate', 'distinction')
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.generatedcertificate': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
'certificate_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
'course_id': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '255'}),
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'download_url': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '128'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'grade': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '5'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'default': 'False', 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'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
# -*- 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):
# Deleting field 'GeneratedCertificate.enabled'
db.delete_column('certificates_generatedcertificate', 'enabled')
# Adding field 'GeneratedCertificate.status'
db.add_column('certificates_generatedcertificate', 'status',
self.gf('django.db.models.fields.CharField')(default='unavailable', max_length=32),
keep_default=False)
def backwards(self, orm):
# Adding field 'GeneratedCertificate.enabled'
db.add_column('certificates_generatedcertificate', 'enabled',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Deleting field 'GeneratedCertificate.status'
db.delete_column('certificates_generatedcertificate', 'status')
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.generatedcertificate': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
'certificate_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '32', 'blank': 'True'}),
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'}),
'distinction': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'download_url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '128', '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'}),
'status': ('django.db.models.fields.CharField', [], {'default': "'unavailable'", 'max_length': '32'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'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
# -*- 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):
# Deleting field 'GeneratedCertificate.certificate_id'
db.delete_column('certificates_generatedcertificate', 'certificate_id')
# Adding field 'GeneratedCertificate.verify_uuid'
db.add_column('certificates_generatedcertificate', 'verify_uuid',
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
keep_default=False)
# Adding field 'GeneratedCertificate.download_uuid'
db.add_column('certificates_generatedcertificate', 'download_uuid',
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
keep_default=False)
def backwards(self, orm):
# Adding field 'GeneratedCertificate.certificate_id'
db.add_column('certificates_generatedcertificate', 'certificate_id',
self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True),
keep_default=False)
# Deleting field 'GeneratedCertificate.verify_uuid'
db.delete_column('certificates_generatedcertificate', 'verify_uuid')
# Deleting field 'GeneratedCertificate.download_uuid'
db.delete_column('certificates_generatedcertificate', 'download_uuid')
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.generatedcertificate': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'GeneratedCertificate'},
'course_id': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', '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'}),
'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'}),
'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
# -*- 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 'GeneratedCertificate.name'
db.add_column('certificates_generatedcertificate', 'name',
self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True),
keep_default=False)
# Adding field 'GeneratedCertificate.created_date'
db.add_column('certificates_generatedcertificate', 'created_date',
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now_add=True, blank=True),
keep_default=False)
# Adding field 'GeneratedCertificate.modified_date'
db.add_column('certificates_generatedcertificate', 'modified_date',
self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'GeneratedCertificate.name'
db.delete_column('certificates_generatedcertificate', 'name')
# Deleting field 'GeneratedCertificate.created_date'
db.delete_column('certificates_generatedcertificate', 'created_date')
# Deleting field 'GeneratedCertificate.modified_date'
db.delete_column('certificates_generatedcertificate', 'modified_date')
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.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'}),
'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
# -*- 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 'GeneratedCertificate.error_reason'
db.add_column('certificates_generatedcertificate', 'error_reason',
self.gf('django.db.models.fields.CharField')(default='', max_length=512, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'GeneratedCertificate.error_reason'
db.delete_column('certificates_generatedcertificate', 'error_reason')
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.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
from django.conf import settings as settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db import models from django.db import models
from datetime import datetime
"""
'''
Certificates are created for a student and an offering of a course. Certificates are created for a student and an offering of a course.
When a certificate is generated, a unique ID is generated so that When a certificate is generated, a unique ID is generated so that
the certificate can be verified later. The ID is a UUID4, so that the certificate can be verified later. The ID is a UUID4, so that
it can't be easily guessed and so that it is unique. Even though it can't be easily guessed and so that it is unique.
we save these generated certificates (for later verification), we
also record the UUID so that if we regenerate the certificate it Certificates are generated in batches by a cron job, when a
will have the same UUID. certificate is available for download the GeneratedCertificate
table is updated with information that will be displayed
If certificates are being generated on the fly, a GeneratedCertificate on the course overview page.
should be created with the user, certificate_id, and enabled set
when a student requests a certificate. When the certificate has been
generated, the download_url should be set. State diagram:
Certificates can also be pre-generated. In this case, the user, [deleted,error,unavailable] [error,downloadable]
certificate_id, and download_url are all set before the user does + + +
anything. When the user requests the certificate, only enabled | | |
needs to be set to true. | | |
add_cert regen_cert del_cert
''' | | |
v v v
[generating] [regenerating] [deleting]
+ + +
| | |
certificate certificate certificate
created removed,created deleted
+----------------+-------------+------->[error]
| | |
| | |
v v v
[downloadable] [downloadable] [deleted]
"""
class CertificateStatuses(object):
unavailable = 'unavailable'
generating = 'generating'
regenerating = 'regenerating'
deleting = 'deleting'
deleted = 'deleted'
downloadable = 'downloadable'
notpassing = 'notpassing'
error = 'error'
class GeneratedCertificate(models.Model): class GeneratedCertificate(models.Model):
user = models.ForeignKey(User, db_index=True) user = models.ForeignKey(User)
# This is the name at the time of request course_id = models.CharField(max_length=255, blank=True, default='')
name = models.CharField(blank=True, max_length=255) verify_uuid = models.CharField(max_length=32, blank=True, default='')
download_uuid = models.CharField(max_length=32, blank=True, default='')
certificate_id = models.CharField(max_length=32, null=True, default=None) download_url = models.CharField(max_length=128, blank=True, default='')
graded_certificate_id = models.CharField(max_length=32, null=True, default=None) grade = models.CharField(max_length=5, blank=True, default='')
key = models.CharField(max_length=32, blank=True, default='')
download_url = models.CharField(max_length=128, null=True) distinction = models.BooleanField(default=False)
graded_download_url = models.CharField(max_length=128, null=True) status = models.CharField(max_length=32, default='unavailable')
grade = models.CharField(max_length=5, null=True)
# enabled should only be true if the student has earned a grade in the course
# The student must have a grade and request a certificate for enabled to be True
enabled = models.BooleanField(default=False)
class RevokedCertificate(models.Model):
"""
This model is for when a GeneratedCertificate must be regenerated. This model
contains all the same fields, to store a record of what the GeneratedCertificate
was before it was revoked (at which time all of it's information can change when
it is regenerated).
GeneratedCertificate may be deleted once they are revoked, and then created again.
For this reason, the only link between a GeneratedCertificate and RevokedCertificate
is that they share the same user.
"""
####-------------------New Fields--------------------####
explanation = models.TextField(blank=True)
####---------Fields from GeneratedCertificate---------####
user = models.ForeignKey(User, db_index=True)
# This is the name at the time of request
name = models.CharField(blank=True, max_length=255) name = models.CharField(blank=True, max_length=255)
created_date = models.DateTimeField(
auto_now_add=True, default=datetime.now)
modified_date = models.DateTimeField(
auto_now=True, default=datetime.now)
error_reason = models.CharField(max_length=512, blank=True, default='')
certificate_id = models.CharField(max_length=32, null=True, default=None) class Meta:
graded_certificate_id = models.CharField(max_length=32, null=True, default=None) unique_together = (('user', 'course_id'),)
download_url = models.CharField(max_length=128, null=True)
graded_download_url = models.CharField(max_length=128, null=True)
grade = models.CharField(max_length=5, null=True)
enabled = models.BooleanField(default=False)
def revoke_certificate(certificate, explanation):
"""
This method takes a GeneratedCertificate. It records its information from the certificate
into a RevokedCertificate, and then marks the certificate as needing regenerating.
When the new certificiate is regenerated it will have new IDs and download URLS.
Once this method has been called, it is safe to delete the certificate, or modify the def certificate_status_for_student(student, course_id):
certificate's name or grade until it has been generated again.
"""
revoked = RevokedCertificate(user=certificate.user,
name=certificate.name,
certificate_id=certificate.certificate_id,
graded_certificate_id=certificate.graded_certificate_id,
download_url=certificate.download_url,
graded_download_url=certificate.graded_download_url,
grade=certificate.grade,
enabled=certificate.enabled)
revoked.explanation = explanation
certificate.certificate_id = None
certificate.graded_certificate_id = None
certificate.download_url = None
certificate.graded_download_url = None
certificate.save()
revoked.save()
def certificate_state_for_student(student, grade):
''' '''
This returns a dictionary with a key for state, and other information. The state is one of the This returns a dictionary with a key for status, and other information.
following: The status is one of the following:
unavailable - A student is not eligible for a certificate.
generating - A request has been made to generate a certificate,
but it has not been generated yet.
regenerating - A request has been made to regenerate a certificate,
but it has not been generated yet.
deleting - A request has been made to delete a certificate.
unavailable - A student is not eligible for a certificate. deleted - The certificate has been deleted.
requestable - A student is eligible to request a certificate downloadable - The certificate is available for download.
generating - A student has requested a certificate, but it is not generated yet. notpassing - The student was graded but is not passing
downloadable - The certificate has been requested and is available for download.
If the state is "downloadable", the dictionary also contains "download_url" and "graded_download_url". If the status is "downloadable", the dictionary also contains
"download_url".
If the student has been graded, the dictionary also contains their
grade for the course.
''' '''
if grade: try:
#TODO: Remove the following after debugging generated_certificate = GeneratedCertificate.objects.get(
if settings.DEBUG_SURVEY: user=student, course_id=course_id)
return {'state': 'requestable'} d = {'status': generated_certificate.status}
if generated_certificate.grade:
try: d['grade'] = generated_certificate.grade
generated_certificate = GeneratedCertificate.objects.get(user=student) if generated_certificate.status == CertificateStatuses.downloadable:
if generated_certificate.enabled: d['download_url'] = generated_certificate.download_url
if generated_certificate.download_url:
return {'state': 'downloadable', return d
'download_url': generated_certificate.download_url, except GeneratedCertificate.DoesNotExist:
'graded_download_url': generated_certificate.graded_download_url} pass
else: return {'status': CertificateStatuses.unavailable}
return {'state': 'generating'}
else:
# If enabled=False, it may have been pre-generated but not yet requested
# Our output will be the same as if the GeneratedCertificate did not exist
pass
except GeneratedCertificate.DoesNotExist:
pass
return {'state': 'requestable'}
else:
# No grade, no certificate. No exceptions
return {'state': 'unavailable'}
from certificates.models import GeneratedCertificate
from certificates.models import certificate_status_for_student
from certificates.models import CertificateStatuses as status
from courseware import grades, courses
from django.test.client import RequestFactory
from capa.xqueue_interface import XQueueInterface
from capa.xqueue_interface import make_xheader, make_hashkey
from django.conf import settings
from requests.auth import HTTPBasicAuth
from student.models import UserProfile
import json
import random
import logging
logger = logging.getLogger(__name__)
class XQueueCertInterface(object):
"""
XQueueCertificateInterface provides an
interface to the xqueue server for
managing student certificates.
Instantiating an object will create a new
connection to the queue server.
See models.py for valid state transitions,
summary of methods:
add_cert: Add a new certificate. Puts a single
request on the queue for the student/course.
Once the certificate is generated a post
will be made to the update_certificate
view which will save the certificate
download URL.
regen_cert: Regenerate an existing certificate.
For a user that already has a certificate
this will delete the existing one and
generate a new cert.
del_cert: Delete an existing certificate
For a user that already has a certificate
this will delete his cert.
"""
def __init__(self, request=None):
# Get basic auth (username/password) for
# xqueue connection if it's in the settings
if settings.XQUEUE_INTERFACE.get('basic_auth') is not None:
requests_auth = HTTPBasicAuth(
*settings.XQUEUE_INTERFACE['basic_auth'])
else:
requests_auth = None
if request is None:
factory = RequestFactory()
self.request = factory.get('/')
else:
self.request = request
self.xqueue_interface = XQueueInterface(
settings.XQUEUE_INTERFACE['url'],
settings.XQUEUE_INTERFACE['django_auth'],
requests_auth,
)
def regen_cert(self, student, course_id):
"""
Arguments:
student - User.object
course_id - courseenrollment.course_id (string)
Removes certificate for a student, will change
the certificate status to 'regenerating'.
Certificate must be in the 'error' or 'downloadable' state
If the student has a passing grade a certificate
request will be put on the queue
If the student is not passing his state will change
to status.notpassing
otherwise it will return the current state
"""
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
def del_cert(self, student, course_id):
"""
Arguments:
student - User.object
course_id - courseenrollment.course_id (string)
Removes certificate for a student, will change
the certificate status to 'deleting'.
Certificate must be in the 'error' or 'downloadable' state
otherwise it will return the current state
"""
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
def add_cert(self, student, course_id):
"""
Arguments:
student - User.object
course_id - courseenrollment.course_id (string)
Request a new certificate for a student.
Will change the certificate status to 'deleting'.
Certificate must be in the 'unavailable', 'error',
or 'deleted' state.
If a student has a passing grade a request will made
for a new cert
If a student does not have a passing grade the status
will change to status.notpassing
Returns the student's status
"""
VALID_STATUSES = [status.unavailable, status.deleted, status.error,
status.notpassing]
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)
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
if grade['grade'] is not None:
cert_status = status.generating
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)
cert.save()
else:
cert_status = status.notpassing
cert.status = cert_status
cert.user = student
cert.course_id = course_id
cert.name = profile.name
cert.save()
return cert_status
def _send_to_xqueue(self, contents, key):
xheader = make_xheader(
'http://{0}/update_certificate?{1}'.format(
settings.SITE_NAME, key), key, settings.CERT_QUEUE)
(error, msg) = self.xqueue_interface.send_to_queue(
header=xheader, body=json.dumps(contents))
if error:
logger.critical('Unable to add a request to the queue')
raise Exception('Unable to send queue message')
import json
import logging import logging
import uuid from certificates.models import GeneratedCertificate
from certificates.models import CertificateStatuses as status
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
import json
from django.conf import settings logger = logging.getLogger(__name__)
from django.contrib.auth.decorators import login_required
from django.core.mail import send_mail
from django.http import Http404, HttpResponse
from django.shortcuts import redirect
import courseware.grades as grades
from certificates.models import GeneratedCertificate, certificate_state_for_student, revoke_certificate
from mitxmako.shortcuts import render_to_response, render_to_string
from student.models import UserProfile
#TODO: Finish migrating these changes from stable
# from student.survey_questions import exit_survey_list_for_student
# from student.views import student_took_survey, record_exit_survey
log = logging.getLogger("mitx.certificates") @csrf_exempt
def update_certificate(request):
"""
Will update GeneratedCertificate for a new certificate or
modify an existing certificate entry.
See models.py for a state diagram of certificate states
@login_required This view should only ever be accessed by the xqueue server
def certificate_request(request): """
''' Attempt to send a certificate. '''
if not settings.END_COURSE_ENABLED:
raise Http404
if request.method == "POST": if request.method == "POST":
honor_code_verify = request.POST.get('cert_request_honor_code_verify', 'false')
name_verify = request.POST.get('cert_request_name_verify', 'false')
id_verify = request.POST.get('cert_request_id_verify', 'false')
error = ''
def return_error(error):
return HttpResponse(json.dumps({'success': False,
'error': error}))
if honor_code_verify != 'true':
error += 'Please verify that you have followed the honor code to receive a certificate. '
if name_verify != 'true':
error += 'Please verify that your name is correct to receive a certificate. '
if id_verify != 'true':
error += 'Please certify that you understand the unique ID on the certificate. '
if len(error) > 0:
return return_error(error)
survey_response = record_exit_survey(request, internal_request=True)
if not survey_response['success']:
return return_error(survey_response['error'])
grade = None
student_gradesheet = grades.grade(request.user, request, course)
grade = student_gradesheet['grade']
if not grade:
return return_error('You have not earned a grade in this course. ')
generate_certificate(request.user, grade)
return HttpResponse(json.dumps({'success': True}))
else:
#This is not a POST, we should render the page with the form
student_gradesheet = grades.grade(request.user, request, course)
certificate_state = certificate_state_for_student(request.user, grade_sheet['grade'])
if certificate_state['state'] != "requestable":
return redirect("/profile")
user_info = UserProfile.objects.get(user=request.user) xqueue_body = json.loads(request.POST.get('xqueue_body'))
xqueue_header = json.loads(request.POST.get('xqueue_header'))
took_survey = student_took_survey(user_info)
if settings.DEBUG_SURVEY:
took_survey = False
survey_list = []
if not took_survey:
survey_list = exit_survey_list_for_student(request.user)
context = {'certificate_state': certificate_state,
'took_survey': took_survey,
'survey_list': survey_list,
'name': user_info.name}
return render_to_response('cert_request.html', context)
# This method should only be called if the user has a grade and has requested a certificate
def generate_certificate(user, grade):
# Make sure to see the comments in models.GeneratedCertificate to read about the valid
# states for a GeneratedCertificate object
if grade and user.is_active:
generated_certificate = None
try: try:
generated_certificate = GeneratedCertificate.objects.get(user=user) cert = GeneratedCertificate.objects.get(
except GeneratedCertificate.DoesNotExist: user__username=xqueue_body['username'],
generated_certificate = GeneratedCertificate(user=user) course_id=xqueue_body['course_id'],
key=xqueue_header['lms_key'])
generated_certificate.enabled = True
if generated_certificate.graded_download_url and (generated_certificate.grade != grade):
log.critical(u"A graded certificate has been pre-generated with the grade "
"of {gen_grade} but requested by user id {userid} with grade "
"{req_grade}! The download URLs were {graded_dl_url} and "
"{ungraded_dl_url}".format(
gen_grade=generated_certificate.grade,
req_grade=grade,
graded_dl_url=generated_certificate.graded_download_url,
ungraded_dl_url=generated_certificate.download_url,
userid=user.id))
revoke_certificate(generated_certificate, "The grade on this certificate may be inaccurate.")
user_name = UserProfile.objects.get(user=user).name except GeneratedCertificate.DoesNotExist:
if generated_certificate.download_url and (generated_certificate.name != user_name): logger.critical('Unable to lookup certificate\n'
log.critical(u"A Certificate has been pre-generated with the name of " 'xqueue_body: {0}\n'
"{gen_name} but current name is {user_name} (user id is " 'xqueue_header: {1}'.format(
"{userid})! The download URLs were {graded_dl_url} and " xqueue_body, xqueue_header))
"{ungraded_dl_url}".format(
gen_name=generated_certificate.name.encode('utf-8'), return HttpResponse(json.dumps({
user_name=user_name.encode('utf-8'), 'return_code': 1,
graded_dl_url=generated_certificate.graded_download_url, 'content': 'unable to lookup key'}),
ungraded_dl_url=generated_certificate.download_url, mimetype='application/json')
userid=user.id))
revoke_certificate(generated_certificate, "The name on this certificate may be inaccurate.") if 'error' in xqueue_body:
cert.status = status.error
generated_certificate.grade = grade if 'error_reason' in xqueue_body:
generated_certificate.name = user_name
generated_certificate.save() # Hopefully we will record a meaningful error
# here if something bad happened during the
certificate_id = generated_certificate.certificate_id # certificate generation process
#
log.debug("Generating certificate for " + str(user.username) + " with ID: " + str(certificate_id)) # example:
# (aamorm BerkeleyX/CS169.1x/2012_Fall)
# TODO: If the certificate was pre-generated, send the email that it is ready to download # <class 'simples3.bucket.S3Error'>:
if certificate_state_for_student(user, grade)['state'] == "downloadable": # HTTP error (reason=error(32, 'Broken pipe'), filename=None) :
subject = render_to_string('emails/certificate_ready_subject.txt', {}) # certificate_agent.py:175
subject = ''.join(subject.splitlines())
message = render_to_string('emails/certificate_ready.txt', {})
cert.error_reason = xqueue_body['error_reason']
res = send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, [user.email, ]) else:
if cert.status in [status.generating, status.regenerating]:
else: cert.download_uuid = xqueue_body['download_uuid']
log.warning("Asked to generate a certificate for student " + str(user.username) + " but with a grade of " + str(grade) + " and active status " + str(user.is_active)) cert.verify_uuid = xqueue_body['verify_uuid']
cert.download_url = xqueue_body['url']
cert.status = status.downloadable
elif cert.status in [status.deleting]:
cert.status = status.deleted
else:
logger.critical('Invalid state for cert update: {0}'.format(
cert.status))
return HttpResponse(json.dumps({
'return_code': 1,
'content': 'invalid cert status'}),
mimetype='application/json')
cert.save()
return HttpResponse(json.dumps({'return_code': 0}),
mimetype='application/json')
...@@ -23,7 +23,7 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage' ...@@ -23,7 +23,7 @@ DEFAULT_FILE_STORAGE = 'storages.backends.s3boto.S3BotoStorage'
MITX_FEATURES['ENABLE_DISCUSSION'] = False MITX_FEATURES['ENABLE_DISCUSSION'] = False
MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True MITX_FEATURES['ENABLE_DISCUSSION_SERVICE'] = True
# IMPORTANT: With this enabled, the server must always be behind a proxy that # IMPORTANT: With this enabled, the server must always be behind a proxy that
# strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise, # strips the header HTTP_X_FORWARDED_PROTO from client requests. Otherwise,
# a user can fool our server into thinking it was an https connection. # a user can fool our server into thinking it was an https connection.
# See https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header # See https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
...@@ -60,6 +60,7 @@ COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {}) ...@@ -60,6 +60,7 @@ COURSE_LISTINGS = ENV_TOKENS.get('COURSE_LISTINGS', {})
SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {}) SUBDOMAIN_BRANDING = ENV_TOKENS.get('SUBDOMAIN_BRANDING', {})
COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL",'') COMMENTS_SERVICE_URL = ENV_TOKENS.get("COMMENTS_SERVICE_URL",'')
COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY",'') COMMENTS_SERVICE_KEY = ENV_TOKENS.get("COMMENTS_SERVICE_KEY",'')
CERT_QUEUE = ENV_TOKENS.get("CERT_QUEUE", 'test-pull')
############################## SECURE AUTH ITEMS ############################### ############################## SECURE AUTH ITEMS ###############################
# Secret things: passwords, access keys, etc. # Secret things: passwords, access keys, etc.
...@@ -80,4 +81,4 @@ if 'COURSE_ID' in ENV_TOKENS: ...@@ -80,4 +81,4 @@ if 'COURSE_ID' in ENV_TOKENS:
ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID']) ASKBOT_URL = "courses/{0}/discussions/".format(ENV_TOKENS['COURSE_ID'])
PEARSON_TEST_USER = "pearsontest" PEARSON_TEST_USER = "pearsontest"
PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD") PEARSON_TEST_PASSWORD = AUTH_TOKENS.get("PEARSON_TEST_PASSWORD")
\ No newline at end of file
...@@ -26,7 +26,6 @@ $sidebar-color: #f6f6f6; ...@@ -26,7 +26,6 @@ $sidebar-color: #f6f6f6;
$outer-border-color: #aaa; $outer-border-color: #aaa;
// old variables // old variables
$light-gray: #ddd; $light-gray: #ddd;
$dark-gray: #333; $dark-gray: #333;
$text-color: $dark-gray; $text-color: $dark-gray;
......
...@@ -132,7 +132,7 @@ img { ...@@ -132,7 +132,7 @@ img {
} }
.site-status { .site-status {
display: block; display: block;
} }
.toast-notification { .toast-notification {
......
...@@ -179,6 +179,7 @@ ...@@ -179,6 +179,7 @@
overflow: hidden; overflow: hidden;
position: relative; position: relative;
width: flex-grid(12); width: flex-grid(12);
z-index: 20;
@include transition(all, 0.15s, linear); @include transition(all, 0.15s, linear);
&:last-child { &:last-child {
...@@ -318,6 +319,19 @@ ...@@ -318,6 +319,19 @@
} }
} }
.course-status-completed {
background: #ccc;
color: #fff;
p {
color: #222;
span {
font-weight: bold;
}
}
}
.enter-course { .enter-course {
@include button(shiny, $blue); @include button(shiny, $blue);
@include box-sizing(border-box); @include box-sizing(border-box);
...@@ -357,10 +371,113 @@ ...@@ -357,10 +371,113 @@
border-color: darken(rgb(200,200,200), 3%); border-color: darken(rgb(200,200,200), 3%);
@include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6)); @include box-shadow(0 1px 0 0 rgba(255,255,255, 0.6));
} }
.course-status-completed {
background: #888;
color: #fff;
}
}
}
}
.message-status {
@include border-radius(3px);
@include box-shadow(0 1px 4px 0 rgba(0,0,0, 0.1), inset 0 -1px 0 0 rgba(255,255,255, 0.8), inset 0 1px 0 0 rgba(255,255,255, 0.8));
display: none;
position: relative;
top: -15px;
z-index: 10;
margin: 0 0 20px 0;
padding: 15px 20px;
font-family: "Open Sans", Verdana, Geneva, sans-serif;
background: #fffcf0;
border: 1px solid #ccc;
.message-copy {
margin: 0;
.grade-value {
font-size: 1.4rem;
font-weight: bold;
}
}
.actions {
@include clearfix;
list-style: none;
margin: 15px 0 0 0;
padding: 0;
.action {
float: left;
margin:0 15px 10px 0;
.btn, .cta {
display: inline-block;
}
.btn {
@include button(shiny, $blue);
@include box-sizing(border-box);
@include border-radius(3px);
float: left;
font: normal 0.8rem/1.2rem $sans-serif;
letter-spacing: 1px;
padding: 6px 12px;
text-align: center;
&.disabled {
@include button(shiny, #eee);
cursor: default !important;
&:hover {
background: #eee;
background-image: -webkit-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
background-image: -moz-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
background-image: -ms-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
background-image: -o-linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
background-image: linear-gradient(top, #EEE 0%, #C2C2C2 50%, #ABABAB 50%, #B0B0B0 100%);
}
}
}
.cta {
@include button(shiny, #666);
float: left;
font: normal 0.8rem/1.2rem $sans-serif;
letter-spacing: 1px;
padding: 6px 12px;
text-align: center;
}
}
}
&.is-shown {
display: block;
}
&.course-status-processing {
}
&.course-status-certnotavailable {
// background: #fee8d6;
}
&.course-status-certrendering {
// background: #d9e7db;
.cta {
margin-top: 2px;
} }
} }
&.course-status-certavailable {
// background: #d9e7db;
}
} }
a.unenroll { a.unenroll {
float: right; float: right;
font-style: italic; font-style: italic;
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from courseware.courses import course_image_url, get_course_about_section from courseware.courses import course_image_url, get_course_about_section
from courseware.access import has_access from courseware.access import has_access
from certificates.models import CertificateStatuses
%> %>
<%inherit file="main.html" /> <%inherit file="main.html" />
...@@ -114,7 +115,7 @@ ...@@ -114,7 +115,7 @@
</header> </header>
% if len(courses) > 0: % if len(courses) > 0:
% for course in courses: % for course, cert_status in zip(courses, cert_statuses):
<article class="my-course"> <article class="my-course">
<% <%
...@@ -135,8 +136,16 @@ ...@@ -135,8 +136,16 @@
<h2 class="university">${get_course_about_section(course, 'university')}</h2> <h2 class="university">${get_course_about_section(course, 'university')}</h2>
<h3>${course.number} ${course.title}</h3> <h3>${course.number} ${course.title}</h3>
</hgroup> </hgroup>
<section class="course-status"> <section class="course-status course-status-completed">
<p>Class Starts - <span>${course.start_date_text}</span></p> <p>
% if course.has_ended():
Course Completed - <span>${course.end_date_text}</span>
% elif course.has_started():
Course Started - <span>${course.start_date_text}</span>
% else: # hasn't started yet
Course Starts - <span>${course.start_date_text}</span>
% endif
</p>
</section> </section>
% if course.id in show_courseware_links_for: % if course.id in show_courseware_links_for:
<p class="enter-course">View Courseware</p> <p class="enter-course">View Courseware</p>
...@@ -144,6 +153,67 @@ ...@@ -144,6 +153,67 @@
</section> </section>
</a> </a>
</article> </article>
% if course.has_ended:
<%
passing_grade = False
cert_button = False
survey_button = False
if cert_status['status'] in [CertificateStatuses.generating, CertificateStatuses.regenerating]:
status_css_class = 'course-status-certrendering'
cert_button = True
survey_button = True
passing_grade = True
elif cert_status['status'] == CertificateStatuses.downloadable:
status_css_class = 'course-status-certavailable'
cert_button = True
survey_button = True
passing_grade = True
elif cert_status['status'] == CertificateStatuses.notpassing:
status_css_class = 'course-status-certnotavailable'
survey_button = True
else:
# This is primarily the 'unavailable' state, but also 'error', 'deleted', etc.
status_css_class = 'course-status-processing'
if survey_button and not course.end_of_course_survey_url:
survey_button = False
%>
<div class="message message-status ${status_css_class} is-shown">
% if cert_status['status'] == CertificateStatuses.unavailable:
<p class="message-copy">Final course details are being wrapped up at this time.
Your final standing will be available shortly.</p>
% elif passing_grade:
<p class="message-copy">You have received a grade of
<span class="grade-value">${cert_status['grade']}</span>
in this course.</p>
% elif cert_status['status'] == CertificateStatuses.notpassing:
<p class="message-copy">You did not complete the necessary requirements for
completion of this course. Your grade was <span class="grade-value">${cert_status['grade']}</span>
</p>
% endif
% if cert_button or survey_button:
<ul class="actions">
% if cert_button and cert_status['status'] in [CertificateStatuses.generating, CertificateStatuses.regenerating]:
<li class="action"><span class="btn disabled" href="">Your Certificate is Generating</span></li>
% elif cert_button and cert_status['status'] == CertificateStatuses.downloadable:
<li class="action">
<a class="btn" href="${cert_status['download_url']}"
title="This link will open/download a PDF document">
Download Your PDF Certificate</a></li>
% endif
% if survey_button:
<li class="action"><a class="cta" href="${course.end_of_course_survey_url}">
Complete our course feedback survey</a></li>
% endif
</ul>
% endif
</div>
% endif
<a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a> <a href="#unenroll-modal" class="unenroll" rel="leanModal" data-course-id="${course.id}" data-course-number="${course.number}">Unregister</a>
% endfor % endfor
......
...@@ -10,6 +10,9 @@ if settings.DEBUG: ...@@ -10,6 +10,9 @@ if settings.DEBUG:
admin.autodiscover() admin.autodiscover()
urlpatterns = ('', urlpatterns = ('',
# certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'),
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment