diff --git a/lms/djangoapps/certificates/badge_handler.py b/lms/djangoapps/certificates/badge_handler.py index 163551c3d4..31eaf4ec27 100644 --- a/lms/djangoapps/certificates/badge_handler.py +++ b/lms/djangoapps/certificates/badge_handler.py @@ -4,9 +4,10 @@ BadgeHandler object-- used to award Badges to users who have completed courses. import hashlib import logging import mimetypes +from eventtracking import tracker +import requests from django.template.defaultfilters import slugify from django.utils.translation import ugettext as _ -import requests from django.conf import settings from django.core.urlresolvers import reverse @@ -120,6 +121,13 @@ class BadgeHandler(object): course_mode=mode, ) + def site_prefix(self): + """ + Get the prefix for the site URL-- protocol and server name. + """ + scheme = u"https" if settings.HTTPS == "on" else u"http" + return u'{}://{}'.format(scheme, settings.SITE_NAME) + def create_badge(self, mode): """ Create the badge spec for a course's mode. @@ -135,27 +143,48 @@ class BadgeHandler(object): ) files = {'image': (image.name, image, content_type)} about_path = reverse('about_course', kwargs={'course_id': unicode(self.course_key)}) - scheme = u"https" if settings.HTTPS == "on" else u"http" data = { 'name': course.display_name, - 'criteria': u'{}://{}{}'.format(scheme, settings.SITE_NAME, about_path), + 'criteria': u'{}{}'.format(self.site_prefix(), about_path), 'slug': self.course_slug(mode), 'description': self.badge_description(course, mode) } result = requests.post(self.badge_create_url, headers=self.get_headers(), data=data, files=files) self.log_if_raised(result, data) + def send_assertion_created_event(self, user, assertion): + """ + Send an analytics event to record the creation of a badge assertion. + """ + tracker.emit( + 'edx.badges.assertion.created', { + 'user_id': user.id, + 'course_id': unicode(self.course_key), + 'enrollment_mode': assertion.mode, + 'assertion_id': assertion.id, + 'assertion_image_url': assertion.data['image'], + 'assertion_json_url': assertion.data['json']['id'], + 'issuer': assertion.data['issuer'], + } + ) + def create_assertion(self, user, mode): """ Register an assertion with the Badgr server for a particular user in a particular course mode for this course. """ - data = {'email': user.email} + data = { + 'email': user.email, + 'evidence': self.site_prefix() + reverse( + 'cert_html_view', kwargs={'user_id': user.id, 'course_id': unicode(self.course_key)} + ) + '?evidence_visit=1' + } response = requests.post(self.assertion_url(mode), headers=self.get_headers(), data=data) self.log_if_raised(response, data) - assertion, __ = BadgeAssertion.objects.get_or_create(course_id=self.course_key, user=user) + assertion, __ = BadgeAssertion.objects.get_or_create(course_id=self.course_key, user=user, mode=mode) assertion.data = response.json() assertion.save() + self.send_assertion_created_event(user, assertion) def award(self, user): """ diff --git a/lms/djangoapps/certificates/models.py b/lms/djangoapps/certificates/models.py index 986e1f21bb..2eb48d68a4 100644 --- a/lms/djangoapps/certificates/models.py +++ b/lms/djangoapps/certificates/models.py @@ -595,6 +595,7 @@ class BadgeAssertion(models.Model): """ Get the image for this assertion. """ + return self.data['image'] @property @@ -608,7 +609,7 @@ class BadgeAssertion(models.Model): """ Meta information for Django's construction of the model. """ - unique_together = (('course_id', 'user'),) + unique_together = (('course_id', 'user', 'mode'),) def validate_badge_image(image): diff --git a/lms/djangoapps/certificates/tests/test_badge_handler.py b/lms/djangoapps/certificates/tests/test_badge_handler.py index a1c626818e..787bfb039f 100644 --- a/lms/djangoapps/certificates/tests/test_badge_handler.py +++ b/lms/djangoapps/certificates/tests/test_badge_handler.py @@ -7,6 +7,7 @@ from django.db.models.fields.files import ImageFieldFile from lazy.lazy import lazy from mock import patch, Mock, call from certificates.models import BadgeAssertion, BadgeImageConfiguration +from openedx.core.lib.tests.assertions.events import assert_event_matches from track.tests import EventTrackingTestCase from xmodule.modulestore.tests.factories import CourseFactory from certificates.badge_handler import BadgeHandler @@ -155,7 +156,7 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase): self.assertTrue(BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b']) @patch('requests.post') - def test_create_assertion(self, post): + def test_badge_creation_event(self, post): result = { 'json': {'id': 'http://www.example.com/example'}, 'image': 'http://www.example.com/example.png', @@ -174,7 +175,22 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase): 'edxcourse_testtest_run_honor_fc5519b/assertions' ) self.check_headers(kwargs['headers']) - self.assertEqual(kwargs['data'], {'email': 'example@example.com'}) - badge = BadgeAssertion.objects.get(user=self.user, course_id=self.course.location.course_key) - self.assertEqual(badge.data, result) - self.assertEqual(badge.image_url, 'http://www.example.com/example.png') + assertion = BadgeAssertion.objects.get(user=self.user, course_id=self.course.location.course_key) + self.assertEqual(assertion.data, result) + self.assertEqual(assertion.image_url, 'http://www.example.com/example.png') + self.assertEqual(kwargs['data'], { + 'email': 'example@example.com', + 'evidence': 'https://edx.org/certificates/user/2/course/edX/course_test/test_run?evidence_visit=1' + }) + assert_event_matches({ + 'name': 'edx.badges.assertion.created', + 'data': { + 'user_id': self.user.id, + 'course_id': unicode(self.course.location.course_key), + 'enrollment_mode': 'honor', + 'assertion_id': assertion.id, + 'assertion_image_url': 'http://www.example.com/example.png', + 'assertion_json_url': 'http://www.example.com/example', + 'issuer': 'https://example.com/v1/issuer/issuers/test-issuer', + } + }, self.get_event()) diff --git a/lms/djangoapps/certificates/tests/test_views.py b/lms/djangoapps/certificates/tests/test_views.py index c9138490df..7e7e55c822 100644 --- a/lms/djangoapps/certificates/tests/test_views.py +++ b/lms/djangoapps/certificates/tests/test_views.py @@ -13,16 +13,20 @@ from django.test.client import Client from django.test.utils import override_settings from opaque_keys.edx.locator import CourseLocator +from openedx.core.lib.tests.assertions.events import assert_event_matches from student.tests.factories import UserFactory +from track.tests import EventTrackingTestCase from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from certificates.api import get_certificate_url -from certificates.models import ExampleCertificateSet, ExampleCertificate, GeneratedCertificate +from certificates.models import ExampleCertificateSet, ExampleCertificate, GeneratedCertificate, BadgeAssertion from certificates.tests.factories import ( CertificateHtmlViewConfigurationFactory, - LinkedInAddToProfileConfigurationFactory + LinkedInAddToProfileConfigurationFactory, + BadgeAssertionFactory, ) +from lms import urls FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True @@ -174,7 +178,7 @@ class UpdateExampleCertificateViewTest(TestCase): @attr('shard_1') -class CertificatesViewsTests(ModuleStoreTestCase): +class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase): """ Tests for the manual refund page """ @@ -416,3 +420,88 @@ class CertificatesViewsTests(ModuleStoreTestCase): ) response = self.client.get(test_url) self.assertIn("Invalid Certificate", response.content) + + def test_evidence_event_sent(self): + test_url = get_certificate_url(user_id=self.user.id, course_id=self.course_id) + '?evidence_visit=1' + self.recreate_tracker() + assertion = BadgeAssertion( + user=self.user, course_id=self.course_id, mode='honor', + data={ + 'image': 'http://www.example.com/image.png', + 'json': {'id': 'http://www.example.com/assertion.json'}, + 'issuer': 'http://www.example.com/issuer.json', + + } + ) + assertion.save() + response = self.client.get(test_url) + self.assertEqual(response.status_code, 200) + assert_event_matches( + { + 'name': 'edx.badges.assertion.evidence_visit', + 'data': { + 'course_id': 'testorg/run1/refundable_course', + # pylint: disable=no-member + 'assertion_id': assertion.id, + 'assertion_json_url': 'http://www.example.com/assertion.json', + 'assertion_image_url': 'http://www.example.com/image.png', + 'user_id': self.user.id, + 'issuer': 'http://www.example.com/issuer.json', + 'enrollment_mode': 'honor', + }, + }, + self.get_event() + ) + + +class TrackShareRedirectTest(ModuleStoreTestCase, EventTrackingTestCase): + """ + Verifies the badge image share event is sent out. + """ + def setUp(self): + super(TrackShareRedirectTest, self).setUp() + self.client = Client() + self.course = CourseFactory.create( + org='testorg', number='run1', display_name='trackable course' + ) + self.assertion = BadgeAssertionFactory( + user=self.user, course_id=self.course.id, data={ + 'image': 'http://www.example.com/image.png', + 'json': {'id': 'http://www.example.com/assertion.json'}, + 'issuer': 'http://www.example.com/issuer.json', + }, + ) + # Enabling the feature flag isn't enough to change the URLs-- they're already loaded by this point. + self.old_patterns = urls.urlpatterns + urls.urlpatterns += (urls.BADGE_SHARE_TRACKER_URL,) + + def tearDown(self): + super(TrackShareRedirectTest, self).tearDown() + urls.urlpatterns = self.old_patterns + + def test_social_event_sent(self): + test_url = '/certificates/badge_share_tracker/{}/social_network/{}/'.format( + unicode(self.course.id), + self.user.username, + ) + self.recreate_tracker() + response = self.client.get(test_url) + self.assertEqual(response.status_code, 302) + self.assertEqual(response['Location'], 'http://www.example.com/image.png') + assert_event_matches( + { + 'name': 'edx.badges.assertion.shared', + 'data': { + 'course_id': 'testorg/run1/trackable_course', + 'social_network': 'social_network', + # pylint: disable=no-member + 'assertion_id': self.assertion.id, + 'assertion_json_url': 'http://www.example.com/assertion.json', + 'assertion_image_url': 'http://www.example.com/image.png', + 'user_id': self.user.id, + 'issuer': 'http://www.example.com/issuer.json', + 'enrollment_mode': 'honor', + }, + }, + self.get_event() + ) diff --git a/lms/djangoapps/certificates/views.py b/lms/djangoapps/certificates/views.py index fa32994ed7..31234570eb 100644 --- a/lms/djangoapps/certificates/views.py +++ b/lms/djangoapps/certificates/views.py @@ -1,12 +1,14 @@ """URL handlers related to certificate handling by LMS""" from datetime import datetime from uuid import uuid4 +from django.shortcuts import redirect, get_object_or_404 +from opaque_keys.edx.locator import CourseLocator +from eventtracking import tracker import dogstats_wrapper as dog_stats_api import json import logging from django.conf import settings -from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.http import HttpResponse, Http404, HttpResponseForbidden from django.utils.translation import ugettext as _ @@ -24,6 +26,7 @@ from certificates.models import ( BadgeAssertion) from certificates.queue import XQueueCertInterface from edxmako.shortcuts import render_to_response +from util.views import ensure_valid_course_key from xmodule.modulestore.django import modulestore from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey @@ -518,6 +521,29 @@ def render_html_view(request, user_id, course_id): except (InvalidKeyError, CourseDoesNotExist, User.DoesNotExist): return render_to_response(invalid_template_path, context) + if 'evidence_visit' in request.GET: + print "Event request found!" + try: + badge = BadgeAssertion.objects.get(user=user, course_id=course_key) + tracker.emit( + 'edx.badges.assertion.evidence_visit', + { + 'user_id': user.id, + 'course_id': unicode(course_key), + 'enrollment_mode': badge.mode, + 'assertion_id': badge.id, + 'assertion_image_url': badge.data['image'], + 'assertion_json_url': badge.data['json']['id'], + 'issuer': badge.data['issuer'], + } + ) + except BadgeAssertion.DoesNotExist: + logger.warn( + "Could not find badge for %s on course %s.", + user.id, + course_key, + ) + # Okay, now we have all of the pieces, time to put everything together # Get the active certificate configuration for this course @@ -541,3 +567,25 @@ def render_html_view(request, user_id, course_id): context.update(course.cert_html_view_overrides) return render_to_response("certificates/valid.html", context) + + +@ensure_valid_course_key +def track_share_redirect(request__unused, course_id, network, student_username): + """ + Tracks when a user downloads a badge for sharing. + """ + course_id = CourseLocator.from_string(course_id) + assertion = get_object_or_404(BadgeAssertion, user__username=student_username, course_id=course_id) + tracker.emit( + 'edx.badges.assertion.shared', { + 'course_id': unicode(course_id), + 'social_network': network, + 'assertion_id': assertion.id, + 'assertion_json_url': assertion.data['json']['id'], + 'assertion_image_url': assertion.image_url, + 'user_id': assertion.user.id, + 'enrollment_mode': assertion.mode, + 'issuer': assertion.data['issuer'], + } + ) + return redirect(assertion.image_url) diff --git a/lms/djangoapps/courseware/tests/test_about.py b/lms/djangoapps/courseware/tests/test_about.py index 0c75863408..5adba1a528 100644 --- a/lms/djangoapps/courseware/tests/test_about.py +++ b/lms/djangoapps/courseware/tests/test_about.py @@ -12,6 +12,7 @@ from nose.plugins.attrib import attr from opaque_keys.edx.locations import SlashSeparatedCourseKey from course_modes.models import CourseMode +from track.tests import EventTrackingTestCase from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_CLOSED_MODULESTORE from student.models import CourseEnrollment @@ -34,7 +35,7 @@ SHIB_ERROR_STR = "The currently logged-in user account does not have permission @attr('shard_1') -class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): +class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, EventTrackingTestCase): """ Tests about xblock. """ diff --git a/lms/urls.py b/lms/urls.py index 93d9ddb1ab..551d26f5b7 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -637,6 +637,17 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): 'certificates.views.render_html_view', name='cert_html_view'), ) +BADGE_SHARE_TRACKER_URL = url( + r'^certificates/badge_share_tracker/{}/(?P[^/]+)/(?P[^/]+)/$'.format( + settings.COURSE_ID_PATTERN + ), + 'certificates.views.track_share_redirect', + name='badge_share_tracker' +) + +if settings.FEATURES.get('ENABLE_OPENBADGES', False): + urlpatterns += (BADGE_SHARE_TRACKER_URL,) + # XDomain proxy urlpatterns += ( url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'),