Analytics events for badges.
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
11
lms/urls.py
11
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<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
|
||||
urlpatterns += (
|
||||
url(r'^xdomain_proxy.html$', 'cors_csrf.views.xdomain_proxy', name='xdomain_proxy'),
|
||||
|
||||
Reference in New Issue
Block a user