Commit 7a56e00d by Diana Huang

Merge pull request #1604 from edx/jarv/verified-certs

Jarv/verified certs
parents 93823db3 586b1f72
...@@ -157,38 +157,43 @@ class CourseEndingTest(TestCase): ...@@ -157,38 +157,43 @@ class CourseEndingTest(TestCase):
{'status': 'processing', {'status': 'processing',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': False, }) 'show_survey_button': False,
})
cert_status = {'status': 'unavailable'} cert_status = {'status': 'unavailable'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'processing', {'status': 'processing',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': False}) 'show_survey_button': False,
'mode': None
})
cert_status = {'status': 'generating', 'grade': '67'} cert_status = {'status': 'generating', 'grade': '67', 'mode': 'honor'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'generating', {'status': 'generating',
'show_disabled_download_button': True, 'show_disabled_download_button': True,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': True, 'show_survey_button': True,
'survey_url': survey_url, 'survey_url': survey_url,
'grade': '67' 'grade': '67',
'mode': 'honor'
}) })
cert_status = {'status': 'regenerating', 'grade': '67'} cert_status = {'status': 'regenerating', 'grade': '67', 'mode': 'verified'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'generating', {'status': 'generating',
'show_disabled_download_button': True, 'show_disabled_download_button': True,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': True, 'show_survey_button': True,
'survey_url': survey_url, 'survey_url': survey_url,
'grade': '67' 'grade': '67',
'mode': 'verified'
}) })
download_url = 'http://s3.edx/cert' download_url = 'http://s3.edx/cert'
cert_status = {'status': 'downloadable', 'grade': '67', cert_status = {'status': 'downloadable', 'grade': '67',
'download_url': download_url} 'download_url': download_url, 'mode': 'honor'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'ready', {'status': 'ready',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
...@@ -196,30 +201,33 @@ class CourseEndingTest(TestCase): ...@@ -196,30 +201,33 @@ class CourseEndingTest(TestCase):
'download_url': download_url, 'download_url': download_url,
'show_survey_button': True, 'show_survey_button': True,
'survey_url': survey_url, 'survey_url': survey_url,
'grade': '67' 'grade': '67',
'mode': 'honor'
}) })
cert_status = {'status': 'notpassing', 'grade': '67', cert_status = {'status': 'notpassing', 'grade': '67',
'download_url': download_url} 'download_url': download_url, 'mode': 'honor'}
self.assertEqual(_cert_info(user, course, cert_status), self.assertEqual(_cert_info(user, course, cert_status),
{'status': 'notpassing', {'status': 'notpassing',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': True, 'show_survey_button': True,
'survey_url': survey_url, 'survey_url': survey_url,
'grade': '67' 'grade': '67',
'mode': 'honor'
}) })
# Test a course that doesn't have a survey specified # Test a course that doesn't have a survey specified
course2 = Mock(end_of_course_survey_url=None) course2 = Mock(end_of_course_survey_url=None)
cert_status = {'status': 'notpassing', 'grade': '67', cert_status = {'status': 'notpassing', 'grade': '67',
'download_url': download_url} 'download_url': download_url, 'mode': 'honor'}
self.assertEqual(_cert_info(user, course2, cert_status), self.assertEqual(_cert_info(user, course2, cert_status),
{'status': 'notpassing', {'status': 'notpassing',
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': False, 'show_survey_button': False,
'grade': '67' 'grade': '67',
'mode': 'honor'
}) })
......
...@@ -185,7 +185,8 @@ def _cert_info(user, course, cert_status): ...@@ -185,7 +185,8 @@ def _cert_info(user, course, cert_status):
default_info = {'status': default_status, default_info = {'status': default_status,
'show_disabled_download_button': False, 'show_disabled_download_button': False,
'show_download_url': False, 'show_download_url': False,
'show_survey_button': False} 'show_survey_button': False,
}
if cert_status is None: if cert_status is None:
return default_info return default_info
...@@ -203,7 +204,8 @@ def _cert_info(user, course, cert_status): ...@@ -203,7 +204,8 @@ def _cert_info(user, course, cert_status):
d = {'status': status, d = {'status': status,
'show_download_url': status == 'ready', 'show_download_url': status == 'ready',
'show_disabled_download_button': status == 'generating', } 'show_disabled_download_button': status == 'generating',
'mode': cert_status.get('mode', None)}
if (status in ('generating', 'ready', 'notpassing', 'restricted') and if (status in ('generating', 'ready', 'notpassing', 'restricted') and
course.end_of_course_survey_url is not None): course.end_of_course_survey_url is not None):
...@@ -296,7 +298,7 @@ def complete_course_mode_info(course_id, enrollment): ...@@ -296,7 +298,7 @@ def complete_course_mode_info(course_id, enrollment):
def dashboard(request): def dashboard(request):
user = request.user user = request.user
# Build our (course, enorllment) list for the user, but ignore any courses that no # Build our (course, enrollment) list for the user, but ignore any courses that no
# longer exist (because the course IDs have changed). Still, we don't delete those # longer exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu. # enrollments, because it could have been a data push snafu.
course_enrollment_pairs = [] course_enrollment_pairs = []
......
...@@ -93,6 +93,7 @@ class Command(BaseCommand): ...@@ -93,6 +93,7 @@ class Command(BaseCommand):
total = enrolled_students.count() total = enrolled_students.count()
count = 0 count = 0
start = datetime.datetime.now(UTC) start = datetime.datetime.now(UTC)
for student in enrolled_students: for student in enrolled_students:
count += 1 count += 1
if count % STATUS_INTERVAL == 0: if count % STATUS_INTERVAL == 0:
......
# -*- 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.mode'
db.add_column('certificates_generatedcertificate', 'mode',
self.gf('django.db.models.fields.CharField')(default='honor', max_length=32),
keep_default=False)
def backwards(self, orm):
# Deleting field 'GeneratedCertificate.mode'
db.delete_column('certificates_generatedcertificate', 'mode')
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'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '32'}),
'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.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 from datetime import datetime
from model_utils import Choices
""" """
Certificates are created for a student and an offering of a course. Certificates are created for a student and an offering of a course.
...@@ -62,7 +63,6 @@ class CertificateStatuses(object): ...@@ -62,7 +63,6 @@ class CertificateStatuses(object):
restricted = 'restricted' restricted = 'restricted'
unavailable = 'unavailable' unavailable = 'unavailable'
class CertificateWhitelist(models.Model): class CertificateWhitelist(models.Model):
""" """
Tracks students who are whitelisted, all users Tracks students who are whitelisted, all users
...@@ -86,6 +86,8 @@ class GeneratedCertificate(models.Model): ...@@ -86,6 +86,8 @@ class GeneratedCertificate(models.Model):
key = models.CharField(max_length=32, blank=True, default='') key = models.CharField(max_length=32, blank=True, default='')
distinction = models.BooleanField(default=False) distinction = models.BooleanField(default=False)
status = models.CharField(max_length=32, default='unavailable') status = models.CharField(max_length=32, default='unavailable')
MODES = Choices('verified', 'honor', 'audit')
mode = models.CharField(max_length=32, choices=MODES, default=MODES.honor)
name = models.CharField(blank=True, max_length=255) name = models.CharField(blank=True, max_length=255)
created_date = models.DateTimeField( created_date = models.DateTimeField(
auto_now_add=True, default=datetime.now) auto_now_add=True, default=datetime.now)
...@@ -129,7 +131,8 @@ def certificate_status_for_student(student, course_id): ...@@ -129,7 +131,8 @@ def certificate_status_for_student(student, course_id):
try: try:
generated_certificate = GeneratedCertificate.objects.get( generated_certificate = GeneratedCertificate.objects.get(
user=student, course_id=course_id) user=student, course_id=course_id)
d = {'status': generated_certificate.status} d = {'status': generated_certificate.status,
'mode': generated_certificate.mode}
if generated_certificate.grade: if generated_certificate.grade:
d['grade'] = generated_certificate.grade d['grade'] = generated_certificate.grade
if generated_certificate.status == CertificateStatuses.downloadable: if generated_certificate.status == CertificateStatuses.downloadable:
...@@ -138,4 +141,4 @@ def certificate_status_for_student(student, course_id): ...@@ -138,4 +141,4 @@ def certificate_status_for_student(student, course_id):
return d return d
except GeneratedCertificate.DoesNotExist: except GeneratedCertificate.DoesNotExist:
pass pass
return {'status': CertificateStatuses.unavailable} return {'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor}
...@@ -9,7 +9,8 @@ from capa.xqueue_interface import XQueueInterface ...@@ -9,7 +9,8 @@ from capa.xqueue_interface import XQueueInterface
from capa.xqueue_interface import make_xheader, make_hashkey from capa.xqueue_interface import make_xheader, make_hashkey
from django.conf import settings from django.conf import settings
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from student.models import UserProfile from student.models import UserProfile, CourseEnrollment
from verify_student.models import SoftwareSecurePhotoVerification
import json import json
import random import random
...@@ -156,6 +157,8 @@ class XQueueCertInterface(object): ...@@ -156,6 +157,8 @@ class XQueueCertInterface(object):
cert_status = certificate_status_for_student(student, course_id)['status'] cert_status = certificate_status_for_student(student, course_id)['status']
new_status = cert_status
if cert_status in VALID_STATUSES: if cert_status in VALID_STATUSES:
# grade the student # grade the student
...@@ -165,9 +168,6 @@ class XQueueCertInterface(object): ...@@ -165,9 +168,6 @@ class XQueueCertInterface(object):
course = courses.get_course_by_id(course_id) course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student) profile = UserProfile.objects.get(user=student)
cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
# Needed # Needed
self.request.user = student self.request.user = student
self.request.session = {} self.request.session = {}
...@@ -175,45 +175,64 @@ class XQueueCertInterface(object): ...@@ -175,45 +175,64 @@ class XQueueCertInterface(object):
grade = grades.grade(student, self.request, course) grade = grades.grade(student, self.request, course)
is_whitelisted = self.whitelist.filter( is_whitelisted = self.whitelist.filter(
user=student, course_id=course_id, whitelist=True).exists() user=student, course_id=course_id, whitelist=True).exists()
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)
org = course_id.split('/')[0]
course_num = course_id.split('/')[1]
cert_mode = enrollment_mode
if enrollment_mode == GeneratedCertificate.MODES.verified and SoftwareSecurePhotoVerification.user_is_verified(student):
template_pdf = "certificate-template-{0}-{1}-verified.pdf".format(
org, course_num)
elif (enrollment_mode == GeneratedCertificate.MODES.verified and not
SoftwareSecurePhotoVerification.user_is_verified(student)):
template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num)
cert_mode = GeneratedCertificate.MODES.honor
else:
# honor code and audit students
template_pdf = "certificate-template-{0}-{1}.pdf".format(
org, course_num)
if is_whitelisted or grade['grade'] is not None: cert, created = GeneratedCertificate.objects.get_or_create(
user=student, course_id=course_id)
key = make_hashkey(random.random())
cert.grade = grade['percent'] cert.mode = cert_mode
cert.user = student cert.user = student
cert.grade = grade['percent']
cert.course_id = course_id cert.course_id = course_id
cert.key = key
cert.name = profile.name cert.name = profile.name
if is_whitelisted or grade['grade'] is not None:
# check to see whether the student is on the # check to see whether the student is on the
# the embargoed country restricted list # the embargoed country restricted list
# otherwise, put a new certificate request # otherwise, put a new certificate request
# on the queue # on the queue
if self.restricted.filter(user=student).exists(): if self.restricted.filter(user=student).exists():
cert.status = status.restricted new_status = status.restricted
cert.status = new_status
cert.save() cert.save()
else: else:
key = make_hashkey(random.random())
cert.key = key
contents = { contents = {
'action': 'create', 'action': 'create',
'username': student.username, 'username': student.username,
'course_id': course_id, 'course_id': course_id,
'name': profile.name, 'name': profile.name,
'grade': grade['grade'], 'grade': grade['grade'],
'template_pdf': template_pdf,
} }
cert.status = status.generating new_status = status.generating
cert.status = new_status
cert.save() cert.save()
self._send_to_xqueue(contents, key) self._send_to_xqueue(contents, key)
else: else:
cert_status = status.notpassing new_status = status.notpassing
cert.grade = grade['percent'] cert.status = new_status
cert.user = student
cert.course_id = course_id
cert.name = profile.name
cert.status = cert_status
cert.save() cert.save()
return cert_status return new_status
def _send_to_xqueue(self, contents, key): def _send_to_xqueue(self, contents, key):
......
...@@ -19,7 +19,7 @@ else: ...@@ -19,7 +19,7 @@ else:
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'): % elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
<p class="message-copy">${_("Your final grade:")} <p class="message-copy">${_("Your final grade:")}
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>. <span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
% if cert_status['status'] == 'notpassing': % if cert_status['status'] == 'notpassing' and enrollment.mode != 'audit':
${_("Grade required for a certificate:")} <span class="grade-value"> ${_("Grade required for a certificate:")} <span class="grade-value">
${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>. ${"{0:.0f}%".format(float(course.lowest_passing_grade)*100)}</span>.
% elif cert_status['status'] == 'restricted' and enrollment.mode == 'verified': % elif cert_status['status'] == 'restricted' and enrollment.mode == 'verified':
...@@ -44,6 +44,12 @@ else: ...@@ -44,6 +44,12 @@ else:
<a class="btn" href="${cert_status['download_url']}" <a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}"> title="${_('This link will open/download a PDF document')}">
${_("Download Your Certificate (PDF)")}</a></li> ${_("Download Your Certificate (PDF)")}</a></li>
% elif cert_status['show_download_url'] and enrollment.mode == 'verified' and cert_status['mode'] == 'honor':
<li class="action">
<p>${_('Since we did not have a valid set of verification photos from you when certificates were generated, we could not grant you a verified certificate. An honor code certificate has been granted instead.')}</p>
<a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}">
${_("Download Your Certificate (PDF)")}</a></li>
% elif cert_status['show_download_url'] and enrollment.mode == 'verified': % elif cert_status['show_download_url'] and enrollment.mode == 'verified':
<li class="action"> <li class="action">
<a class="btn" href="${cert_status['download_url']}" <a class="btn" href="${cert_status['download_url']}"
......
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