Commit 9cc30d09 by Joe Blaylock

Merge pull request #2533 from edx/jrbl/certs_request_cert_endpoint

/request_cert AJAX endpoint
parents 3ff3973b 09745079
......@@ -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,
......
......@@ -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
......
---
metadata:
display_name: (Grade Me!) Button
data: |
<p>By clicking the button below, you assert that you have completed the course in its entirety.</p>
<input type=button value="Yes, I Agree." id="User_Verify_Button" style="margin-bottom: 20px;" />
<p class="verify-button-success-text" style="font-weight: bold; color: #008200;"></p>
<script type="text/javascript">
var success_message = "Your grading and certification request has been received, <br />if you have passed, your certificate should be available in the next 20 minutes.";
document.getElementById('User_Verify_Button').addEventListener("click",
function(event) {
(function(event) {
var linkcontents = $('a.user-link').contents();
$.ajax({
type: 'POST',
url: '/request_certificate',
data: {'course_id': $$course_id},
success: function(data) {
$('.verify-button-success-text').html(success_message);
}
});
}).call(document.getElementById('User_Verify_Button'), event);
});
</script>
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}">
<chapter url="hi" url_name="ch" display_name="CH">
<html url_name="h" display_name="H">Two houses, ...</html>
</chapter>
</course>
'''.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 = """
# <course org="{org}" course="{course}" display_organization="{org}_display" display_coursenumber="{course}_display"
# graceperiod="1 day" url_name="test"
# start="2012-01-01T12:00"
# {end}
# certificates_show_before_end={cert}>
# <chapter url="hi" url_name="ch" display_name="CH">
# <html url_name="h" display_name="H">Two houses, ...</html>
# </chapter>
# </course>
#""".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"""
......
......@@ -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
......
"""URL handlers related to certificate handling by LMS"""
from dogapi import dog_stats_api
import json
import logging
from certificates.models import GeneratedCertificate
from certificates.models import CertificateStatuses as status
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.models import User
from django.http import HttpResponse
import json
from dogapi import dog_stats_api
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):
"""
Will update GeneratedCertificate for a new certificate or
......@@ -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'))
......
......@@ -24,8 +24,11 @@ else:
%>
<div class="message message-status ${status_css_class} is-shown">
% if cert_status['status'] == 'processing':
% if cert_status['status'] == 'processing' and not course.may_certify():
<p class="message-copy">${_("Final course details are being wrapped up at this time. Your final standing will be available shortly.")}</p>
% elif course.may_certify() and cert_status['status'] == 'processing':
<!-- Certification is allowed but no cert requested, or cert unearned -->
<!-- <p class="message-copy">${_("Your final standing is unrequested or unavailable at this time.")}</p> -->
% elif cert_status['status'] in ('generating', 'ready', 'notpassing', 'restricted'):
<p class="message-copy">${_("Your final grade:")}
<span class="grade-value">${"{0:.0f}%".format(float(cert_status['grade'])*100)}</span>.
......
......@@ -67,7 +67,7 @@
</h3>
</hgroup>
% 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
......
......@@ -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"),
......
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