Commit d499b224 by Jonathan Piacenti

Analytics events for badges.

parent f7862c1d
...@@ -4,9 +4,10 @@ BadgeHandler object-- used to award Badges to users who have completed courses. ...@@ -4,9 +4,10 @@ BadgeHandler object-- used to award Badges to users who have completed courses.
import hashlib import hashlib
import logging import logging
import mimetypes import mimetypes
from eventtracking import tracker
import requests
from django.template.defaultfilters import slugify from django.template.defaultfilters import slugify
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
import requests
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -120,6 +121,13 @@ class BadgeHandler(object): ...@@ -120,6 +121,13 @@ class BadgeHandler(object):
course_mode=mode, 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): def create_badge(self, mode):
""" """
Create the badge spec for a course's mode. Create the badge spec for a course's mode.
...@@ -135,27 +143,48 @@ class BadgeHandler(object): ...@@ -135,27 +143,48 @@ class BadgeHandler(object):
) )
files = {'image': (image.name, image, content_type)} files = {'image': (image.name, image, content_type)}
about_path = reverse('about_course', kwargs={'course_id': unicode(self.course_key)}) about_path = reverse('about_course', kwargs={'course_id': unicode(self.course_key)})
scheme = u"https" if settings.HTTPS == "on" else u"http"
data = { data = {
'name': course.display_name, '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), 'slug': self.course_slug(mode),
'description': self.badge_description(course, mode) 'description': self.badge_description(course, mode)
} }
result = requests.post(self.badge_create_url, headers=self.get_headers(), data=data, files=files) result = requests.post(self.badge_create_url, headers=self.get_headers(), data=data, files=files)
self.log_if_raised(result, data) 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): def create_assertion(self, user, mode):
""" """
Register an assertion with the Badgr server for a particular user in a particular course mode for Register an assertion with the Badgr server for a particular user in a particular course mode for
this course. 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) response = requests.post(self.assertion_url(mode), headers=self.get_headers(), data=data)
self.log_if_raised(response, 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.data = response.json()
assertion.save() assertion.save()
self.send_assertion_created_event(user, assertion)
def award(self, user): def award(self, user):
""" """
......
...@@ -595,6 +595,7 @@ class BadgeAssertion(models.Model): ...@@ -595,6 +595,7 @@ class BadgeAssertion(models.Model):
""" """
Get the image for this assertion. Get the image for this assertion.
""" """
return self.data['image'] return self.data['image']
@property @property
...@@ -608,7 +609,7 @@ class BadgeAssertion(models.Model): ...@@ -608,7 +609,7 @@ class BadgeAssertion(models.Model):
""" """
Meta information for Django's construction of the 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): def validate_badge_image(image):
......
...@@ -7,6 +7,7 @@ from django.db.models.fields.files import ImageFieldFile ...@@ -7,6 +7,7 @@ from django.db.models.fields.files import ImageFieldFile
from lazy.lazy import lazy from lazy.lazy import lazy
from mock import patch, Mock, call from mock import patch, Mock, call
from certificates.models import BadgeAssertion, BadgeImageConfiguration from certificates.models import BadgeAssertion, BadgeImageConfiguration
from openedx.core.lib.tests.assertions.events import assert_event_matches
from track.tests import EventTrackingTestCase from track.tests import EventTrackingTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from certificates.badge_handler import BadgeHandler from certificates.badge_handler import BadgeHandler
...@@ -155,7 +156,7 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -155,7 +156,7 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
self.assertTrue(BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b']) self.assertTrue(BadgeHandler.badges['edxcourse_testtest_run_honor_fc5519b'])
@patch('requests.post') @patch('requests.post')
def test_create_assertion(self, post): def test_badge_creation_event(self, post):
result = { result = {
'json': {'id': 'http://www.example.com/example'}, 'json': {'id': 'http://www.example.com/example'},
'image': 'http://www.example.com/example.png', 'image': 'http://www.example.com/example.png',
...@@ -174,7 +175,22 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase): ...@@ -174,7 +175,22 @@ class BadgeHandlerTestCase(ModuleStoreTestCase, EventTrackingTestCase):
'edxcourse_testtest_run_honor_fc5519b/assertions' 'edxcourse_testtest_run_honor_fc5519b/assertions'
) )
self.check_headers(kwargs['headers']) self.check_headers(kwargs['headers'])
self.assertEqual(kwargs['data'], {'email': 'example@example.com'}) assertion = BadgeAssertion.objects.get(user=self.user, course_id=self.course.location.course_key)
badge = BadgeAssertion.objects.get(user=self.user, course_id=self.course.location.course_key) self.assertEqual(assertion.data, result)
self.assertEqual(badge.data, result) self.assertEqual(assertion.image_url, 'http://www.example.com/example.png')
self.assertEqual(badge.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())
...@@ -13,16 +13,20 @@ from django.test.client import Client ...@@ -13,16 +13,20 @@ from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
from opaque_keys.edx.locator import CourseLocator 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 student.tests.factories import UserFactory
from track.tests import EventTrackingTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from certificates.api import get_certificate_url 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 ( from certificates.tests.factories import (
CertificateHtmlViewConfigurationFactory, CertificateHtmlViewConfigurationFactory,
LinkedInAddToProfileConfigurationFactory LinkedInAddToProfileConfigurationFactory,
BadgeAssertionFactory,
) )
from lms import urls
FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy() FEATURES_WITH_CERTS_ENABLED = settings.FEATURES.copy()
FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True FEATURES_WITH_CERTS_ENABLED['CERTIFICATES_HTML_VIEW'] = True
...@@ -174,7 +178,7 @@ class UpdateExampleCertificateViewTest(TestCase): ...@@ -174,7 +178,7 @@ class UpdateExampleCertificateViewTest(TestCase):
@attr('shard_1') @attr('shard_1')
class CertificatesViewsTests(ModuleStoreTestCase): class CertificatesViewsTests(ModuleStoreTestCase, EventTrackingTestCase):
""" """
Tests for the manual refund page Tests for the manual refund page
""" """
...@@ -416,3 +420,88 @@ class CertificatesViewsTests(ModuleStoreTestCase): ...@@ -416,3 +420,88 @@ class CertificatesViewsTests(ModuleStoreTestCase):
) )
response = self.client.get(test_url) response = self.client.get(test_url)
self.assertIn("Invalid Certificate", response.content) 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()
)
"""URL handlers related to certificate handling by LMS""" """URL handlers related to certificate handling by LMS"""
from datetime import datetime from datetime import datetime
from uuid import uuid4 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 dogstats_wrapper as dog_stats_api
import json import json
import logging import logging
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import HttpResponse, Http404, HttpResponseForbidden from django.http import HttpResponse, Http404, HttpResponseForbidden
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
...@@ -24,6 +26,7 @@ from certificates.models import ( ...@@ -24,6 +26,7 @@ from certificates.models import (
BadgeAssertion) BadgeAssertion)
from certificates.queue import XQueueCertInterface from certificates.queue import XQueueCertInterface
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
...@@ -518,6 +521,29 @@ def render_html_view(request, user_id, course_id): ...@@ -518,6 +521,29 @@ def render_html_view(request, user_id, course_id):
except (InvalidKeyError, CourseDoesNotExist, User.DoesNotExist): except (InvalidKeyError, CourseDoesNotExist, User.DoesNotExist):
return render_to_response(invalid_template_path, context) 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 # Okay, now we have all of the pieces, time to put everything together
# Get the active certificate configuration for this course # Get the active certificate configuration for this course
...@@ -541,3 +567,25 @@ def render_html_view(request, user_id, course_id): ...@@ -541,3 +567,25 @@ def render_html_view(request, user_id, course_id):
context.update(course.cert_html_view_overrides) context.update(course.cert_html_view_overrides)
return render_to_response("certificates/valid.html", context) 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)
...@@ -12,6 +12,7 @@ from nose.plugins.attrib import attr ...@@ -12,6 +12,7 @@ from nose.plugins.attrib import attr
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from course_modes.models import CourseMode from course_modes.models import CourseMode
from track.tests import EventTrackingTestCase
from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_CLOSED_MODULESTORE from xmodule.modulestore.tests.django_utils import TEST_DATA_MIXED_CLOSED_MODULESTORE
from student.models import CourseEnrollment from student.models import CourseEnrollment
...@@ -34,7 +35,7 @@ SHIB_ERROR_STR = "The currently logged-in user account does not have permission ...@@ -34,7 +35,7 @@ SHIB_ERROR_STR = "The currently logged-in user account does not have permission
@attr('shard_1') @attr('shard_1')
class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase): class AboutTestCase(LoginEnrollmentTestCase, ModuleStoreTestCase, EventTrackingTestCase):
""" """
Tests about xblock. Tests about xblock.
""" """
......
...@@ -637,6 +637,17 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): ...@@ -637,6 +637,17 @@ if settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False):
'certificates.views.render_html_view', name='cert_html_view'), 'certificates.views.render_html_view', name='cert_html_view'),
) )
BADGE_SHARE_TRACKER_URL = url(
r'^certificates/badge_share_tracker/{}/(?P<network>[^/]+)/(?P<student_username>[^/]+)/$'.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 # XDomain proxy
urlpatterns += ( urlpatterns += (
url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'), url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'),
......
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