Commit f854726e by Will Daly

Add tracking code to LinkedIn URL.

Add client-side analytics event for LinkedIn Add to Profile button click.

Add management command for generating certs for testing.

Update styling of the certificate messages.
parent 36855475
......@@ -29,6 +29,17 @@ class CourseAccessRoleAdmin(admin.ModelAdmin):
'id', 'user', 'org', 'course_id', 'role'
)
class LinkedInAddToProfileConfigurationAdmin(admin.ModelAdmin):
"""Admin interface for the LinkedIn Add to Profile configuration. """
class Meta:
model = LinkedInAddToProfileConfiguration
# Exclude deprecated fields
exclude = ('dashboard_tracking_code',)
admin.site.register(UserProfile)
admin.site.register(UserTestGroup)
......@@ -45,4 +56,4 @@ admin.site.register(CourseAccessRole, CourseAccessRoleAdmin)
admin.site.register(DashboardConfiguration, ConfigurationModelAdmin)
admin.site.register(LinkedInAddToProfileConfiguration)
admin.site.register(LinkedInAddToProfileConfiguration, LinkedInAddToProfileConfigurationAdmin)
# -*- 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 'LinkedInAddToProfileConfiguration.trk_partner_name'
db.add_column('student_linkedinaddtoprofileconfiguration', 'trk_partner_name',
self.gf('django.db.models.fields.CharField')(default='', max_length=10, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'LinkedInAddToProfileConfiguration.trk_partner_name'
db.delete_column('student_linkedinaddtoprofileconfiguration', 'trk_partner_name')
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'})
},
'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'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', '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']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.dashboardconfiguration': {
'Meta': {'object_name': 'DashboardConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
'student.linkedinaddtoprofileconfiguration': {
'Meta': {'object_name': 'LinkedInAddToProfileConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'company_identifier': ('django.db.models.fields.TextField', [], {}),
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usersignupsource': {
'Meta': {'object_name': 'UserSignupSource'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']
......@@ -1467,34 +1467,88 @@ class LinkedInAddToProfileConfiguration(ConfigurationModel):
# Deprecated
dashboard_tracking_code = models.TextField(default="", blank=True)
def add_to_profile_url(self, course_name, enrollment_mode, cert_url, source="o"):
trk_partner_name = models.CharField(
max_length=10,
default="",
blank=True,
help_text=ugettext_lazy(
u"Short identifier for the LinkedIn partner used in the tracking code. "
u"(Example: 'edx') "
u"If no value is provided, tracking codes will not be sent to LinkedIn."
)
)
def add_to_profile_url(self, course_key, course_name, cert_mode, cert_url, source="o", target="dashboard"):
"""Construct the URL for the "add to profile" button.
Arguments:
course_key (CourseKey): The identifier for the course.
course_name (unicode): The display name of the course.
enrollment_mode (str): The enrollment mode of the user (e.g. "verified", "honor", "professional")
cert_mode (str): The course mode of the user's certificate (e.g. "verified", "honor", "professional")
cert_url (str): The download URL for the certificate.
Keyword Arguments:
source (str): Either "o" (for onsite/UI), "e" (for emails), or "m" (for mobile)
target (str): An identifier for the occurrance of the button.
"""
params = OrderedDict([
('_ed', self.company_identifier),
('pfCertificationName', self._cert_name(course_name, enrollment_mode).encode('utf-8')),
('pfCertificationName', self._cert_name(course_name, cert_mode).encode('utf-8')),
('pfCertificationUrl', cert_url),
('source', source)
])
tracking_code = self._tracking_code(course_key, cert_mode, target)
if tracking_code is not None:
params['trk'] = tracking_code
return u'http://www.linkedin.com/profile/add?{params}'.format(
params=urlencode(params)
)
def _cert_name(self, course_name, enrollment_mode):
def _cert_name(self, course_name, cert_mode):
"""Name of the certification, for display on LinkedIn. """
return self.MODE_TO_CERT_NAME.get(
enrollment_mode,
cert_mode,
_(u"{platform_name} Certificate for {course_name}")
).format(
platform_name=settings.PLATFORM_NAME,
course_name=course_name
)
def _tracking_code(self, course_key, cert_mode, target):
"""Create a tracking code for the button.
Tracking codes are used by LinkedIn to collect
analytics about certifications users are adding
to their profiles.
The tracking code format is:
&trk=[partner name]-[certificate type]-[date]-[target field]
In our case, we're sending:
&trk=edx-{COURSE ID}_{COURSE MODE}-{TARGET}
If no partner code is configured, then this will
return None, indicating that tracking codes are disabled.
Arguments:
course_key (CourseKey): The identifier for the course.
cert_mode (str): The enrollment mode for the course.
target (str): Identifier for where the button is located.
Returns:
unicode or None
"""
return (
u"{partner}-{course_key}_{cert_mode}-{target}".format(
partner=self.trk_partner_name,
course_key=unicode(course_key),
cert_mode=cert_mode,
target=target
)
if self.trk_partner_name else None
)
# -*- coding: utf-8 -*-
"""Tests for LinkedIn Add to Profile configuration. """
import ddt
from urllib import urlencode
from django.test import TestCase
from opaque_keys.edx.locator import CourseLocator
from student.models import LinkedInAddToProfileConfiguration
@ddt.ddt
class LinkedInAddToProfileUrlTests(TestCase):
"""Tests for URL generation of LinkedInAddToProfileConfig. """
COURSE_KEY = CourseLocator(org="edx", course="DemoX", run="Demo_Course")
COURSE_NAME = u"Test Course ☃"
CERT_URL = u"http://s3.edx/cert"
@ddt.data(
('honor', u'edX+Honor+Code+Certificate+for+Test+Course+%E2%98%83'),
('verified', u'edX+Verified+Certificate+for+Test+Course+%E2%98%83'),
('professional', u'edX+Professional+Certificate+for+Test+Course+%E2%98%83'),
('default_mode', u'edX+Certificate+for+Test+Course+%E2%98%83')
)
@ddt.unpack
def test_linked_in_url(self, cert_mode, expected_cert_name):
config = LinkedInAddToProfileConfiguration(
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
enabled=True
)
expected_url = (
'http://www.linkedin.com/profile/add'
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
'pfCertificationName={expected_cert_name}&'
'pfCertificationUrl=http%3A%2F%2Fs3.edx%2Fcert&'
'source=o'
).format(expected_cert_name=expected_cert_name)
actual_url = config.add_to_profile_url(
self.COURSE_KEY,
self.COURSE_NAME,
cert_mode,
self.CERT_URL
)
self.assertEqual(actual_url, expected_url)
def test_linked_in_url_tracking_code(self):
config = LinkedInAddToProfileConfiguration(
company_identifier="abcd123",
trk_partner_name="edx",
enabled=True
)
expected_param = urlencode({
'trk': u'edx-{course_key}_honor-dashboard'.format(
course_key=self.COURSE_KEY
)
})
actual_url = config.add_to_profile_url(
self.COURSE_KEY,
self.COURSE_NAME,
'honor',
self.CERT_URL
)
self.assertIn(expected_param, actual_url)
......@@ -17,7 +17,6 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory, Client
from django.test.utils import override_settings
from mock import Mock, patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......@@ -49,13 +48,6 @@ log = logging.getLogger(__name__)
class CourseEndingTest(TestCase):
"""Test things related to course endings: certificates, surveys, etc"""
def setUp(self):
super(CourseEndingTest, self).setUp()
# Clear the model-based config cache to avoid
# interference between tests.
cache.clear()
def test_process_survey_link(self):
username = "fred"
user = Mock(username=username)
......@@ -198,118 +190,6 @@ class CourseEndingTest(TestCase):
}
self.assertIsNone(_cert_info(user, course2, cert_status, course_mode))
def test_linked_in_url_with_unicode_course_display_name(self):
"""Test with unicode display name values."""
user = Mock(username="fred")
survey_url = "http://a_survey.com"
course = Mock(
end_of_course_survey_url=survey_url,
certificates_display_behavior='end',
display_name=u'edx/abc/courseregisters®'
)
download_url = 'http://s3.edx/cert'
cert_status = {
'status': 'downloadable', 'grade': '67',
'download_url': download_url,
'mode': 'honor'
}
LinkedInAddToProfileConfiguration(
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
enabled=True
).save()
status_dict = _cert_info(user, course, cert_status, 'honor')
expected_url = (
'http://www.linkedin.com/profile/add'
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
'pfCertificationName=edX+Honor+Code+Certificate+for+edx%2Fabc%2Fcourseregisters%C2%AE&'
'pfCertificationUrl=http%3A%2F%2Fs3.edx%2Fcert&'
'source=o'
)
self.assertEqual(expected_url, status_dict['linked_in_url'])
def test_linked_in_url_not_exists_without_config(self):
user = Mock(username="fred")
survey_url = "http://a_survey.com"
course = Mock(
display_name="Demo Course",
end_of_course_survey_url=survey_url,
certificates_display_behavior='end'
)
download_url = 'http://s3.edx/cert'
cert_status = {
'status': 'downloadable', 'grade': '67',
'download_url': download_url,
'mode': 'verified'
}
self.assertEqual(
_cert_info(user, course, cert_status, 'verified'),
{
'status': 'ready',
'show_disabled_download_button': False,
'show_download_url': True,
'download_url': download_url,
'show_survey_button': True,
'survey_url': survey_url,
'grade': '67',
'mode': 'verified',
'linked_in_url': None
}
)
# Enabling the configuration will cause the LinkedIn
# "add to profile" button to appear.
# We need to clear the cache again to make sure we
# pick up the modified configuration.
cache.clear()
LinkedInAddToProfileConfiguration(
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
enabled=True
).save()
status_dict = _cert_info(user, course, cert_status, 'honor')
expected_url = (
'http://www.linkedin.com/profile/add'
'?_ed=0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9&'
'pfCertificationName=edX+Verified+Certificate+for+Demo+Course&'
'pfCertificationUrl=http%3A%2F%2Fs3.edx%2Fcert&'
'source=o'
)
self.assertEqual(expected_url, status_dict['linked_in_url'])
@ddt.data(
('honor', 'edX Honor Code Certificate for DemoX'),
('verified', 'edX Verified Certificate for DemoX'),
('professional', 'edX Professional Certificate for DemoX'),
('default_mode', 'edX Certificate for DemoX')
)
@ddt.unpack
def test_linked_in_url_certificate_types(self, cert_mode, cert_name):
user = Mock(username="fred")
course = Mock(
display_name='DemoX',
end_of_course_survey_url='http://example.com',
certificates_display_behavior='end'
)
cert_status = {
'status': 'downloadable',
'grade': '67',
'download_url': 'http://edx.org',
'mode': cert_mode
}
LinkedInAddToProfileConfiguration(
company_identifier="abcd123",
enabled=True
).save()
status_dict = _cert_info(user, course, cert_status, cert_mode)
self.assertIn(cert_name.replace(' ', '+'), status_dict['linked_in_url'])
class DashboardTest(ModuleStoreTestCase):
"""
......@@ -518,10 +398,7 @@ class DashboardTest(ModuleStoreTestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_linked_in_add_to_profile_btn_not_appearing_without_config(self):
"""
without linked-in config don't show Add Certificate to LinkedIn button
"""
# Without linked-in config don't show Add Certificate to LinkedIn button
self.client.login(username="jack", password="test")
CourseModeFactory.create(
......@@ -557,12 +434,8 @@ class DashboardTest(ModuleStoreTestCase):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_linked_in_add_to_profile_btn_with_certificate(self):
"""
If user has a certificate with valid linked-in config then Add Certificate to LinkedIn button
should be visible. and it has URL value with valid parameters.
"""
# If user has a certificate with valid linked-in config then Add Certificate to LinkedIn button
# should be visible. and it has URL value with valid parameters.
self.client.login(username="jack", password="test")
LinkedInAddToProfileConfiguration(
company_identifier='0_mC_o2MizqdtZEmkVXjH4eYwMj4DnkCWrZP_D9',
......
......@@ -361,6 +361,7 @@ def _cert_info(user, course, cert_status, course_mode):
linkedin_config = LinkedInAddToProfileConfiguration.current()
if linkedin_config.enabled:
status_dict['linked_in_url'] = linkedin_config.add_to_profile_url(
course.id,
course.display_name,
cert_status.get('mode'),
cert_status['download_url']
......
"""Utility for testing certificate display.
This command will create a fake certificate for a user
in a course. The certificate will display on the student's
dashboard, but no PDF will be generated.
Example usage:
$ ./manage.py lms create_fake_cert test_user edX/DemoX/Demo_Course --mode honor --grade 0.89
"""
import logging
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import User
from optparse import make_option
from opaque_keys.edx.keys import CourseKey
from certificates.models import GeneratedCertificate, CertificateStatuses
LOGGER = logging.getLogger(__name__)
class Command(BaseCommand):
"""Create a fake certificate for a user in a course. """
USAGE = u'Usage: create_fake_cert <USERNAME> <COURSE_KEY> --mode <MODE> --status <STATUS> --grade <GRADE>'
option_list = BaseCommand.option_list + (
make_option(
'-m', '--mode',
metavar='CERT_MODE',
dest='cert_mode',
default='honor',
help='The course mode of the certificate (e.g. "honor", "verified", or "professional")'
),
make_option(
'-s', '--status',
metavar='CERT_STATUS',
dest='status',
default=CertificateStatuses.downloadable,
help='The status of the certificate'
),
make_option(
'-g', '--grade',
metavar='CERT_GRADE',
dest='grade',
default='',
help='The grade for the course, as a decimal (e.g. "0.89" for 89%)'
),
)
def handle(self, *args, **options):
"""Create a fake certificate for a user.
Arguments:
username (unicode): Identifier for the certificate's user.
course_key (unicode): Identifier for the certificate's course.
Keyword Arguments:
cert_mode (str): The mode of the certificate (e.g "honor")
status (str): The status of the certificate (e.g. "downloadable")
grade (str): The grade of the certificate (e.g "0.89" for 89%)
Raises:
CommandError
"""
if len(args) < 2:
raise CommandError(self.USAGE)
user = User.objects.get(username=args[0])
course_key = CourseKey.from_string(args[1])
cert_mode = options.get('cert_mode', 'honor')
status = options.get('status', CertificateStatuses.downloadable)
grade = options.get('grade', '')
cert, created = GeneratedCertificate.objects.get_or_create(
user=user,
course_id=course_key
)
cert.mode = cert_mode
cert.status = status
cert.grade = grade
if status == CertificateStatuses.downloadable:
cert.download_uuid = 'test'
cert.verify_uuid = 'test'
cert.download_url = 'http://www.example.com'
cert.save()
if created:
LOGGER.info(
u"Created certificate for user %s in course %s "
u"with mode %s, status %s, "
u"and grade %s",
user.id, unicode(course_key),
cert_mode, status, grade
)
else:
LOGGER.info(
u"Updated certificate for user %s in course %s "
u"with mode %s, status %s, "
u"and grade %s",
user.id, unicode(course_key),
cert_mode, status, grade
)
"""Tests for the create_fake_certs management command. """
from django.test import TestCase
from django.core.management.base import CommandError
from opaque_keys.edx.locator import CourseLocator
from student.tests.factories import UserFactory
from certificates.management.commands import create_fake_cert
from certificates.models import GeneratedCertificate
class CreateFakeCertTest(TestCase):
"""Tests for the create_fake_certs management command. """
USERNAME = "test"
COURSE_KEY = CourseLocator(org='edX', course='DemoX', run='Demo_Course')
def setUp(self):
super(CreateFakeCertTest, self).setUp()
self.user = UserFactory.create(username=self.USERNAME)
def test_create_fake_cert(self):
# No existing cert, so create it
self._run_command(
self.USERNAME,
unicode(self.COURSE_KEY),
cert_mode='verified',
grade='0.89'
)
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY)
self.assertEqual(cert.status, 'downloadable')
self.assertEqual(cert.mode, 'verified')
self.assertEqual(cert.grade, '0.89')
self.assertEqual(cert.download_uuid, 'test')
self.assertEqual(cert.download_url, 'http://www.example.com')
# Cert already exists; modify it
self._run_command(
self.USERNAME,
unicode(self.COURSE_KEY),
cert_mode='honor'
)
cert = GeneratedCertificate.objects.get(user=self.user, course_id=self.COURSE_KEY)
self.assertEqual(cert.mode, 'honor')
def test_too_few_args(self):
with self.assertRaisesRegexp(CommandError, 'Usage'):
self._run_command(self.USERNAME)
def _run_command(self, *args, **kwargs):
"""Run the management command to generate a fake cert. """
command = create_fake_cert.Command()
return command.handle(*args, **kwargs)
......@@ -51,6 +51,21 @@
// Track clicks of the "verify now" button.
window.analytics.trackLink(verifyButtonLinks, 'edx.bi.user.verification.resumed', generateProperties);
// Track clicks of the LinkedIn "Add to Profile" button
window.analytics.trackLink(
$('.action-linkedin-profile'),
'edx.bi.user.linkedin_add_to_profile',
function( element ) {
var $el = $( element );
return {
category: 'linkedin',
label: $el.data('course-id'),
mode: $el.data('certificate-mode')
};
}
);
// Generate the properties object to be passed along with business intelligence events.
function generateProperties(element) {
var $el = $(element),
......
......@@ -612,8 +612,11 @@
// ====================
// UI: message
.message {
.wrapper-message-primary {
@include clearfix();
}
.message {
border-radius: 3px;
display: none;
z-index: 10;
......@@ -887,13 +890,18 @@
}
&.course-status-processing {
background-color: $gray-l5;
border: 0;
}
&.course-status-certnotavailable {
background-color: $gray-l5;
border: 0;
}
&.course-status-certrendering {
background-color: $gray-l5;
border: 0;
.cta {
margin-top: 2px;
......@@ -907,10 +915,10 @@
.message-copy {
width: flex-grid(6, 12);
position: relative;
float: left;
@include float(left);
}
.actions {
.actions-primary {
width: flex-grid(6, 12);
position: relative;
@include float(right);
......@@ -950,6 +958,24 @@
}
}
.actions-secondary {
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/2);
.action-share {
@include float(right);
margin: 0;
}
}
.certificate-explanation {
@extend %t-copy-sub1;
margin-top: ($baseline/2);
border-top: 1px solid $gray-l4;
padding-top: ($baseline/2);
}
.verification-reminder {
width: flex-grid(8, 12);
position: relative;
......
<%page args="cert_status, course, enrollment" />
<%! from django.utils.translation import ugettext as _ %>
<%namespace name='static' file='../static_content.html'/>
<%
cert_name_short = course.cert_name_short
......@@ -45,7 +46,8 @@ else:
% endif
% if cert_status['show_disabled_download_button'] or cert_status['show_download_url'] or cert_status['show_survey_button']:
<ul class="actions">
<div class="wrapper-message-primary">
<ul class="actions actions-primary">
% if cert_status['show_disabled_download_button']:
<li class="action"><span class="disabled">
${_("Your {cert_name_short} is Generating").format(cert_name_short=cert_name_short)}</span></li>
......@@ -54,17 +56,8 @@ else:
<a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}">
${_("Download {cert_name_short} (PDF)").format(cert_name_short=cert_name_short,)}</a></li>
% if cert_status['linked_in_url']:
<li class="action action-share">
<a class="btn" target="_blank" href="${cert_status['linked_in_url']}"
title="${_('Add Certificate to LinkedIn Profile')}">
${_("Share on LinkedIn")}</a></li>
% endif
% 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 your {cert_name_long} was generated, we could not grant you a verified {cert_name_short}. An honor code {cert_name_short} has been granted instead.').format(cert_name_short=cert_name_short, cert_name_long=cert_name_long)}</p>
<a class="btn" href="${cert_status['download_url']}"
title="${_('This link will open/download a PDF document')}">
${_("Download Your {cert_name_short} (PDF)").format(cert_name_short=cert_name_short)}</a></li>
......@@ -80,6 +73,29 @@ else:
${_("Complete our course feedback survey")}</a></li>
% endif
</ul>
</div>
% if cert_status['show_download_url'] and cert_status['linked_in_url']:
<ul class="actions actions-secondary">
<li class="action action-share">
<a class="action action-linkedin-profile" target="_blank" href="${cert_status['linked_in_url']}"
title="${_('Add Certificate to LinkedIn Profile')}"
data-course-id="${unicode(course.id)}"
data-certificate-mode="${cert_status['mode']}"
>
<img class="action-linkedin-profile-img"
src="${static.url('images/linkedin_add_to_profile.png')}"
alt="${_('Share on LinkedIn')}">
</a>
</li>
</ul>
% endif
% if cert_status['show_download_url'] and enrollment.mode == 'verified' and cert_status['mode'] == 'honor':
<div class="certificate-explanation">
${_('Since we did not have a valid set of verification photos from you when your {cert_name_long} was generated, we could not grant you a verified {cert_name_short}. An honor code {cert_name_short} has been granted instead.').format(cert_name_short=cert_name_short, cert_name_long=cert_name_long)}
</div>
% endif
% endif
</div>
......@@ -136,7 +136,7 @@ from student.helpers import (
% endif
% if verification_status.get('status') in [VERIFY_STATUS_NEED_TO_VERIFY, VERIFY_STATUS_SUBMITTED, VERIFY_STATUS_APPROVED, VERIFY_STATUS_NEED_TO_REVERIFY] and not is_course_blocked:
<div class="message message-status is-shown">
<div class="message message-status wrapper-message-primary is-shown">
% if verification_status['status'] == VERIFY_STATUS_NEED_TO_VERIFY:
<div class="verification-reminder">
% if verification_status['days_until_deadline'] is not None:
......
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