diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py
index 34c30c4503..6e02a2a29b 100644
--- a/common/djangoapps/student/views.py
+++ b/common/djangoapps/student/views.py
@@ -200,7 +200,7 @@ def cert_info(user, course):
'survey_url': url, only if show_survey_button is True
'grade': if status is not 'processing'
"""
- if not course.has_ended():
+ if not course.may_certify():
return {}
return _cert_info(user, course, certificate_status_for_student(user, course.id))
@@ -291,6 +291,15 @@ def _cert_info(user, course, cert_status):
"""
Implements the logic for cert_info -- split out for testing.
"""
+ # simplify the status for the template using this lookup table
+ template_state = {
+ CertificateStatuses.generating: 'generating',
+ CertificateStatuses.regenerating: 'generating',
+ CertificateStatuses.downloadable: 'ready',
+ CertificateStatuses.notpassing: 'notpassing',
+ CertificateStatuses.restricted: 'restricted',
+ }
+
default_status = 'processing'
default_info = {'status': default_status,
@@ -302,15 +311,6 @@ def _cert_info(user, course, cert_status):
if cert_status is None:
return default_info
- # simplify the status for the template using this lookup table
- template_state = {
- CertificateStatuses.generating: 'generating',
- CertificateStatuses.regenerating: 'generating',
- CertificateStatuses.downloadable: 'ready',
- CertificateStatuses.notpassing: 'notpassing',
- CertificateStatuses.restricted: 'restricted',
- }
-
status = template_state.get(cert_status['status'], default_status)
d = {'status': status,
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index 3afc44b85b..5eba808cb4 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -369,6 +369,9 @@ class CourseFields(object):
)
enrollment_domain = String(help="External login method associated with user accounts allowed to register in course",
scope=Scope.settings)
+ certificates_show_before_end = Boolean(help="True if students may download certificates before course end",
+ scope=Scope.settings,
+ default=False)
course_image = String(
help="Filename of the course image",
scope=Scope.settings,
@@ -592,6 +595,12 @@ class CourseDescriptor(CourseFields, SequenceDescriptor):
return datetime.now(UTC()) > self.end
+ def may_certify(self):
+ """
+ Return True if it is acceptable to show the student a certificate download link
+ """
+ return self.certificates_show_before_end or self.has_ended()
+
def has_started(self):
return datetime.now(UTC()) > self.start
diff --git a/common/lib/xmodule/xmodule/tests/test_course_module.py b/common/lib/xmodule/xmodule/tests/test_course_module.py
index 7a60e29e37..e336426c14 100644
--- a/common/lib/xmodule/xmodule/tests/test_course_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_course_module.py
@@ -1,5 +1,5 @@
import unittest
-from datetime import datetime
+from datetime import datetime, timedelta
from fs.memoryfs import MemoryFS
@@ -49,7 +49,7 @@ class DummySystem(ImportSystem):
)
-def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None):
+def get_dummy_course(start, announcement=None, is_new=None, advertised_start=None, end=None, certs=False):
"""Get a dummy course"""
system = DummySystem(load_error_modules=True)
@@ -69,17 +69,61 @@ def get_dummy_course(start, announcement=None, is_new=None, advertised_start=Non
{announcement}
{is_new}
{advertised_start}
- {end}>
+ {end}
+ certificates_show_before_end="{certs}">
Two houses, ...
'''.format(org=ORG, course=COURSE, start=start, is_new=is_new,
- announcement=announcement, advertised_start=advertised_start, end=end)
+ announcement=announcement, advertised_start=advertised_start, end=end,
+ certs=certs)
return system.process_xml(start_xml)
+class HasEndedMayCertifyTestCase(unittest.TestCase):
+ """Double check the semantics around when to finalize courses."""
+
+ def setUp(self):
+ system = DummySystem(load_error_modules=True)
+ #sample_xml = """
+ #
+ #
+ # Two houses, ...
+ #
+ #
+ #""".format(org=ORG, course=COURSE)
+ past_end = (datetime.now() - timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
+ future_end = (datetime.now() + timedelta(days=12)).strftime("%Y-%m-%dT%H:%M:00")
+ self.past_show_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs=True)
+ self.past_noshow_certs = get_dummy_course("2012-01-01T12:00", end=past_end, certs=False)
+ self.future_show_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs=True)
+ self.future_noshow_certs = get_dummy_course("2012-01-01T12:00", end=future_end, certs=False)
+ #self.past_show_certs = system.process_xml(sample_xml.format(end=past_end, cert=True))
+ #self.past_noshow_certs = system.process_xml(sample_xml.format(end=past_end, cert=False))
+ #self.future_show_certs = system.process_xml(sample_xml.format(end=future_end, cert=True))
+ #self.future_noshow_certs = system.process_xml(sample_xml.format(end=future_end, cert=False))
+
+ def test_has_ended(self):
+ """Check that has_ended correctly tells us when a course is over."""
+ self.assertTrue(self.past_show_certs.has_ended())
+ self.assertTrue(self.past_noshow_certs.has_ended())
+ self.assertFalse(self.future_show_certs.has_ended())
+ self.assertFalse(self.future_noshow_certs.has_ended())
+
+ def test_may_certify(self):
+ """Check that may_certify correctly tells us when a course may wrap."""
+ self.assertTrue(self.past_show_certs.may_certify())
+ self.assertTrue(self.past_noshow_certs.may_certify())
+ self.assertTrue(self.future_show_certs.may_certify())
+ self.assertFalse(self.future_noshow_certs.may_certify())
+
+
class IsNewCourseTestCase(unittest.TestCase):
"""Make sure the property is_new works on courses"""
diff --git a/lms/djangoapps/certificates/queue.py b/lms/djangoapps/certificates/queue.py
index 0a1f3ea63f..9588ae7c34 100644
--- a/lms/djangoapps/certificates/queue.py
+++ b/lms/djangoapps/certificates/queue.py
@@ -15,6 +15,8 @@ from verify_student.models import SoftwareSecurePhotoVerification
import json
import random
import logging
+import lxml
+from lxml.etree import XMLSyntaxError, ParserError
from xmodule.modulestore import Location
@@ -170,6 +172,7 @@ class XQueueCertInterface(object):
if course is None:
course = courses.get_course_by_id(course_id)
profile = UserProfile.objects.get(user=student)
+ profile_name = profile.name
# Needed
self.request.user = student
@@ -201,9 +204,16 @@ class XQueueCertInterface(object):
cert.user = student
cert.grade = grade['percent']
cert.course_id = course_id
- cert.name = profile.name
+ cert.name = profile_name
+ # Strip HTML from grade range label
+ grade_contents = grade.get('grade', None)
+ try:
+ grade_contents = lxml.html.fromstring(grade_contents).text_content()
+ except (TypeError, XMLSyntaxError, ParserError) as e:
+ # Despite blowing up the xml parser, bad values here are fine
+ grade_contents = None
- if is_whitelisted or grade['grade'] is not None:
+ if is_whitelisted or grade_contents is not None:
# check to see whether the student is on the
# the embargoed country restricted list
@@ -222,8 +232,8 @@ class XQueueCertInterface(object):
'username': student.username,
'course_id': course_id,
'course_name': course_name,
- 'name': profile.name,
- 'grade': grade['grade'],
+ 'name': profile_name,
+ 'grade': grade_contents,
'template_pdf': template_pdf,
}
if template_file:
@@ -233,8 +243,8 @@ class XQueueCertInterface(object):
cert.save()
self._send_to_xqueue(contents, key)
else:
- new_status = status.notpassing
- cert.status = new_status
+ cert_status = status.notpassing
+ cert.status = cert_status
cert.save()
return new_status
diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py
index 11f567a58f..ded4b95627 100644
--- a/lms/djangoapps/certificates/views.py
+++ b/lms/djangoapps/certificates/views.py
@@ -1,15 +1,46 @@
-import logging
-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
+"""URL handlers related to certificate handling by LMS"""
from dogapi import dog_stats_api
+import json
+import logging
+
+from django.contrib.auth.models import User
+from django.http import HttpResponse
+from django.views.decorators.csrf import csrf_exempt
+
from capa.xqueue_interface import XQUEUE_METRIC_NAME
+from certificates.models import certificate_status_for_student, CertificateStatuses, GeneratedCertificate
+from certificates.queue import XQueueCertInterface
+from xmodule.course_module import CourseDescriptor
+from xmodule.modulestore.django import modulestore
logger = logging.getLogger(__name__)
+@csrf_exempt
+def request_certificate(request):
+ """Request the on-demand creation of a certificate for some user, course.
+
+ A request doesn't imply a guarantee that such a creation will take place.
+ We intentionally use the same machinery as is used for doing certification
+ at the end of a course run, so that we can be sure users get graded and
+ then if and only if they pass, do they get a certificate issued.
+ """
+ if request.method == "POST":
+ if request.user.is_authenticated():
+ xqci = XQueueCertInterface()
+ username = request.user.username
+ student = User.objects.get(username=username)
+ course_id = request.POST.get('course_id')
+ course = modulestore().get_instance(course_id, CourseDescriptor.id_to_location(course_id), depth=2)
+
+ status = certificate_status_for_student(student, course_id)['status']
+ if status in [CertificateStatuses.unavailable, CertificateStatuses.notpassing, CertificateStatuses.error]:
+ logger.info('Grading and certification requested for user {} in course {} via /request_certificate call'.format(username, course_id))
+ status = xqci.add_cert(student, course_id, course=course)
+ return HttpResponse(json.dumps({'add_status': status}), mimetype='application/json')
+ return HttpResponse(json.dumps({'add_status': 'ERRORANONYMOUSUSER'}), mimetype='application/json')
+
+
@csrf_exempt
def update_certificate(request):
"""
@@ -21,6 +52,7 @@ def update_certificate(request):
This view should only ever be accessed by the xqueue server
"""
+ status = CertificateStatuses
if request.method == "POST":
xqueue_body = json.loads(request.POST.get('xqueue_body'))
diff --git a/lms/templates/dashboard/_dashboard_certificate_information.html b/lms/templates/dashboard/_dashboard_certificate_information.html
index 951091f371..d8b3977574 100644
--- a/lms/templates/dashboard/_dashboard_certificate_information.html
+++ b/lms/templates/dashboard/_dashboard_certificate_information.html
@@ -24,8 +24,11 @@ else:
%>
-% if cert_status['status'] == 'processing':
+% if cert_status['status'] == 'processing' and not course.may_certify():
${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}
+% elif course.may_certify() and cert_status['status'] == 'processing':
+
+
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
${_("Your final grade:")}
${"{0:.0f}%".format(float(cert_status['grade'])*100)}.
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html
index 0e8e749e69..f415830668 100644
--- a/lms/templates/dashboard/_dashboard_course_listing.html
+++ b/lms/templates/dashboard/_dashboard_course_listing.html
@@ -67,7 +67,7 @@
- % if course.has_ended() and cert_status and not enrollment.mode == 'audit':
+ % if course.may_certify() and cert_status and not enrollment.mode == 'audit':
<%include file='_dashboard_certificate_information.html' args='cert_status=cert_status,course=course, enrollment=enrollment'/>
% endif
diff --git a/lms/urls.py b/lms/urls.py
index e9a033130a..b489295468 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -12,6 +12,7 @@ if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
urlpatterns = ('', # nopep8
# certificate view
url(r'^update_certificate$', 'certificates.views.update_certificate'),
+ url(r'^request_certificate$', 'certificates.views.request_certificate'),
url(r'^$', 'branding.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^token$', 'student.views.token', name="token"),