Commit 5d799f51 by Bill DeRusha

EXPERIMENT: Physical Certificates

parent a7888b78
""" """
Signal handler for enabling/disabling self-generated certificates based on the course-pacing. Signal handler for enabling/disabling self-generated certificates based on the course-pacing.
""" """
import datetime
import logging import logging
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
import pytz
from certificates.models import ( from certificates.models import (
CertificateWhitelist, CertificateWhitelist,
CertificateStatuses,
GeneratedCertificate GeneratedCertificate
) )
from certificates.tasks import generate_certificate from certificates.tasks import generate_certificate, send_passing_learner_message
from certificates.views.shipping_information import PHYSICAL_CERTIFICATE_EXPERIMENT_ID, \
PHYSICAL_CERTIFICATE_EXPERIMENT_KEY
from experiments.models import ExperimentData, ExperimentKeyValue
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from openedx.core.djangoapps.certificates.api import auto_certificate_generation_enabled from openedx.core.djangoapps.certificates.api import auto_certificate_generation_enabled
from openedx.core.djangoapps.certificates.config import waffle
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, LEARNER_NOW_VERIFIED from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, LEARNER_NOW_VERIFIED, COURSE_CERT_AWARDED
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -102,3 +105,62 @@ def fire_ungenerated_certificate_task(user, course_key, expected_verification_st ...@@ -102,3 +105,62 @@ def fire_ungenerated_certificate_task(user, course_key, expected_verification_st
kwargs['expected_verification_status'] = unicode(expected_verification_status) kwargs['expected_verification_status'] = unicode(expected_verification_status)
generate_certificate.apply_async(countdown=CERTIFICATE_DELAY_SECONDS, kwargs=kwargs) generate_certificate.apply_async(countdown=CERTIFICATE_DELAY_SECONDS, kwargs=kwargs)
return True return True
@receiver(COURSE_CERT_AWARDED)
def handle_course_cert_awarded(sender, user, course_key, mode, status, **kwargs): # pylint: disable=unused-argument
log.warn('handle cert award')
try:
exp_data = ExperimentData.objects.get(
user=user,
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key='ship_cert_{0}'.format(str(course_key)),
)
except ExperimentData.DoesNotExist:
return
if exp_data.value != '1':
return
send_passing_learner_message.apply_async((user.id, str(course_key)), retry=False)
@receiver(post_save, sender=CourseEnrollment, dispatch_uid='check_verified_upgrade')
def create_schedule(sender, **kwargs):
enrollment = kwargs['instance']
try:
exp_data = ExperimentData.objects.get(
user=enrollment.user,
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key='showed_interest_{0}'.format(str(enrollment.course_id)),
)
except ExperimentData.DoesNotExist:
return
if exp_data.value != '1':
return
if enrollment.mode not in GeneratedCertificate.VERIFIED_CERTS_MODES:
return
try:
end_time_str = ExperimentKeyValue.objects.get(
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key='end_time'
)
except ExperimentKeyValue.DoesNotExist:
return
end_time = datetime.datetime.strptime(end_time_str, "%Y-%m-%dT%H:%M:%S.%fZ")
if datetime.datetime.now(pytz.utc) >= end_time:
return
ship_exp_data = ExperimentData.objects.get_or_create(
user=enrollment.user,
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key='ship_cert_{0}'.format(str(enrollment.course_id)),
defaults={'value': '1'},
)
ship_exp_data.value = '1'
ship_exp_data.save()
from celery import task from celery import task
from logging import getLogger from logging import getLogger
import logging
from celery_utils.logged_task import LoggedTask from celery_utils.logged_task import LoggedTask
from celery_utils.persist_on_failure import PersistOnFailureTask from celery_utils.persist_on_failure import PersistOnFailureTask
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings
from django.core.urlresolvers import reverse
from django.utils.http import urlquote
from edx_ace import ace
from edx_ace.message import MessageType
from edx_ace.recipient import Recipient
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from .api import generate_user_certificates from .api import generate_user_certificates
from certificates.views.shipping_information import shipping_information
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -43,3 +53,39 @@ def generate_certificate(self, **kwargs): ...@@ -43,3 +53,39 @@ def generate_certificate(self, **kwargs):
if expected_verification_status != actual_verification_status: if expected_verification_status != actual_verification_status:
raise self.retry(kwargs=original_kwargs) raise self.retry(kwargs=original_kwargs)
generate_user_certificates(student=student, course_key=course_key, **kwargs) generate_user_certificates(student=student, course_key=course_key, **kwargs)
ACE_ROUTING_KEY = getattr(settings, 'ACE_ROUTING_KEY', None)
class PassedCourse(MessageType):
pass
@task(ignore_result=True, routing_key=ACE_ROUTING_KEY)
def send_passing_learner_message(user_id, course_key_str):
try:
user = User.objects.get(id=user_id)
course_key = CourseKey.from_string(course_key_str)
course = CourseOverview.get_from_id(course_key)
def absolute_url(relative_path):
return u'{}{}'.format(settings.LMS_ROOT_URL, urlquote(relative_path))
context = {
'shipping_address_form_url': absolute_url(reverse('certificates:shipping_information')),
'course_name': course.display_name,
}
msg = PassedCourse().personalize(
Recipient(
user.username,
user.email,
),
course.language,
context,
)
ace.send(msg)
except:
logger.exception('')
{% extends 'schedules/edx_ace/common/base_body.html' %}
{% load i18n %}
{% block preview_text %}
{% blocktrans trimmed %}
Congratulations on passing {{course_name}}!
{% endblocktrans %}
{% endblock %}
{% block content %}
<table width="100%" align="left" border="0" cellpadding="0" cellspacing="0" role="presentation">
<tr>
<td>
<h1>{% trans "Congratulations!" %}</h1>
<p>
{% blocktrans trimmed %}
You've passed <strong>{{ course_name }}</strong>! Your digital certificate is now available to
share on your LinkedIn profile. If you would like us to ship you your official printed certificate
you will need to tell us where to send it using the link below.
{% endblocktrans %}
</p>
<p>
{# email client support for style sheets is pretty spotty, so we have to inline all of these styles #}
<a
href="{{ shipping_address_form_url }}"
style="
color: #ffffff;
text-decoration: none;
border-radius: 4px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
background-color: #005686;
border-top: 10px solid #005686;
border-bottom: 10px solid #005686;
border-right: 16px solid #005686;
border-left: 16px solid #005686;
display: inline-block;
"
>
{# old email clients require the use of the font tag :( #}
<font color="#ffffff"><b>{% trans "Ship an official certificate to me" %}</b></font>
</a>
</p>
</td>
</tr>
</table>
{% endblock %}
{% load i18n %}
{% if course_ids|length > 1 %}
{% blocktrans trimmed %}
Remember when you enrolled in {{ course_name }}, and other courses on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% trans "Start learning now" %} <{{ dashboard_url }}>
{% else %}
{% blocktrans trimmed %}
Remember when you enrolled in {{ course_name }} on edX.org? We do, and we’re glad
to have you! Come see what everyone is learning.
{% endblocktrans %}
{% trans "Start learning now" %} <{{ course_url }}>
{% endif %}
{% load i18n %}
{% blocktrans %}You've passed {{course_name}}!{% endblocktrans %}
\ No newline at end of file
...@@ -6,6 +6,7 @@ from django.conf import settings ...@@ -6,6 +6,7 @@ from django.conf import settings
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from certificates import views from certificates import views
from certificates.views.shipping_information import shipping_information
urlpatterns = patterns( urlpatterns = patterns(
'', '',
...@@ -30,4 +31,5 @@ urlpatterns = patterns( ...@@ -30,4 +31,5 @@ urlpatterns = patterns(
url(r'search', views.search_certificates, name="search"), url(r'search', views.search_certificates, name="search"),
url(r'regenerate', views.regenerate_certificate_for_user, name="regenerate_certificate_for_user"), url(r'regenerate', views.regenerate_certificate_for_user, name="regenerate_certificate_for_user"),
url(r'generate', views.generate_certificate_for_user, name="generate_certificate_for_user"), url(r'generate', views.generate_certificate_for_user, name="generate_certificate_for_user"),
url(r'shipping_information', shipping_information, name="shipping_information"),
) )
# pylint: disable=bad-continuation
"""
Certificate Shipping Information view.
"""
import json
import logging
from django.contrib.auth.decorators import login_required
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from edxmako.shortcuts import render_to_response
from experiments.models import ExperimentData
log = logging.getLogger(__name__)
PHYSICAL_CERTIFICATE_EXPERIMENT_ID = 100
PHYSICAL_CERTIFICATE_EXPERIMENT_KEY = "shipping_information"
@login_required
def shipping_information(request, template_name='certificates/shipping_information.html'):
default_shipping_json = {
'first_name': "",
'last_name': "",
'address': "",
'city': "",
'state': "",
'zip_code': ""
}
if request.method == 'GET':
shipping_information, created = ExperimentData.objects.get_or_create(
user=request.user,
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key=PHYSICAL_CERTIFICATE_EXPERIMENT_KEY,
defaults={'value': json.dumps(default_shipping_json)},
)
shipping_json = json.loads(shipping_information.value)
if request.method == 'POST':
first_name = request.POST.get('first_name')
last_name = request.POST.get('last_name')
address = request.POST.get('address')
city = request.POST.get('city')
state = request.POST.get('state')
zip_code = request.POST.get('zip_code')
shipping_json = {
'first_name': first_name,
'last_name': last_name,
'address': address,
'city': city,
'state': state,
'zip_code': zip_code
}
shipping_information = ExperimentData.objects.get(
user=request.user,
experiment_id=PHYSICAL_CERTIFICATE_EXPERIMENT_ID,
key=PHYSICAL_CERTIFICATE_EXPERIMENT_KEY
)
shipping_information.value = json.dumps(shipping_json)
shipping_information.save()
return render_to_response(template_name, {'object': shipping_json})
...@@ -1989,7 +1989,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase): ...@@ -1989,7 +1989,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
'failed': 3, 'failed': 3,
'skipped': 2 'skipped': 2
} }
with self.assertNumQueries(106): with self.assertNumQueries(116):
self.assertCertificatesGenerated(task_input, expected_results) self.assertCertificatesGenerated(task_input, expected_results)
expected_results = { expected_results = {
......
<%page expression_filter="h"/>
<%inherit file="../main.html" />
<%block name="pagetitle">Certificate Shipping Information</%block>
<script type="text/javascript">
function editMode(){
var $editable = $('.editable');
var $readOnly = $('.read-only');
$readOnly.addClass('hidden');
$editable.removeClass('hidden');
console.log('called');
};
</script>
<style>
.read-only {
line-height: 25px;
}
#shipping_form {
margin: 25px;
}
label {
display: block;
float: left;
width: 100px;
font-style: normal;
font-weight: bold;
cursor: default;
}
.clearfix {
margin-bottom: 10px;
}
</style>
<h1>
Course Certificate Shipping Information
</h1>
<div id="shipping_form">
<form method="POST">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
<label style="">First Name</label>
<span class="read-only">${object['first_name']}</span>
<input class="editable hidden" type="text" name="first_name" value="${object['first_name']}" />
<div class="clearfix"></div>
<label>Last Name</label>
<span class="read-only">${object['last_name']}</span>
<input class="editable hidden" type="text" name="last_name" value="${object['last_name']}"/>
<div class="clearfix"></div>
<label>Address</label>
<span class="read-only">${object['address']}</span>
<input class="editable hidden" type="text" name="address" value="${object['address']}"/>
<div class="clearfix"></div>
<label>City</label>
<span class="read-only">${object['city']}</span>
<input class="editable hidden" type="text" name="city" value="${object['city']}"/>
<div class="clearfix"></div>
<label>State</label>
<span class="read-only">${object['state']}</span>
<input class="editable hidden" type="text" name="state" value="${object['state']}"/>
<div class="clearfix"></div>
<label>Zip Code</label>
<span class="read-only">${object['zip_code']}</span>
<input class="editable hidden" type="text" name="zip_code" value="${object['zip_code']}"/>
<div class="clearfix"></div>
<br /><br />
<button type="button" class="read-only" id="edit-btn" onclick="editMode(); return false;">edit</button>
<input class="editable hidden" type="submit" name="submit" value="submit" />
</form>
</div>
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