Merge pull request #12450 from edx/peter-fogg/referral-tracking
Add referral tracking for new registrations.
This commit is contained in:
@@ -415,3 +415,6 @@ if FEATURES.get('CUSTOM_COURSES_EDX'):
|
||||
|
||||
# Partner support link for CMS footer
|
||||
PARTNER_SUPPORT_EMAIL = ENV_TOKENS.get('PARTNER_SUPPORT_EMAIL', PARTNER_SUPPORT_EMAIL)
|
||||
|
||||
# Affiliate cookie tracking
|
||||
AFFILIATE_COOKIE_NAME = ENV_TOKENS.get('AFFILIATE_COOKIE_NAME', AFFILIATE_COOKIE_NAME)
|
||||
|
||||
@@ -1193,3 +1193,6 @@ USERNAME_PATTERN = r'(?P<username>[\w.@+-]+)'
|
||||
|
||||
# Partner support link for CMS footer
|
||||
PARTNER_SUPPORT_EMAIL = ''
|
||||
|
||||
# Affiliate cookie tracking
|
||||
AFFILIATE_COOKIE_NAME = 'affiliate_id'
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
from django.conf import settings
|
||||
import model_utils.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('student', '0002_auto_20151208_1034'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UserAttribute',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, verbose_name='created', editable=False)),
|
||||
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, verbose_name='modified', editable=False)),
|
||||
('name', models.CharField(help_text='Name of this user attribute.', max_length=255)),
|
||||
('value', models.CharField(help_text='Value of this user attribute.', max_length=255)),
|
||||
('user', models.ForeignKey(related_name='attributes', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='userattribute',
|
||||
unique_together=set([('user', 'name')]),
|
||||
),
|
||||
]
|
||||
@@ -40,6 +40,7 @@ from django.core.cache import cache
|
||||
from django_countries.fields import CountryField
|
||||
import dogstats_wrapper as dog_stats_api
|
||||
from eventtracking import tracker
|
||||
from model_utils.models import TimeStampedModel
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from simple_history.models import HistoricalRecords
|
||||
@@ -2154,3 +2155,46 @@ class EnrollmentRefundConfiguration(ConfigurationModel):
|
||||
def refund_window(self, refund_window):
|
||||
"""Set the current refund window to the given timedelta."""
|
||||
self.refund_window_microseconds = int(refund_window.total_seconds() * 1000000)
|
||||
|
||||
|
||||
class UserAttribute(TimeStampedModel):
|
||||
"""
|
||||
Record additional metadata about a user, stored as key/value pairs of text.
|
||||
"""
|
||||
|
||||
class Meta(object):
|
||||
# Ensure that at most one value exists for a given user/name.
|
||||
unique_together = (('user', 'name'))
|
||||
|
||||
user = models.ForeignKey(User, related_name='attributes')
|
||||
name = models.CharField(max_length=255, help_text=_("Name of this user attribute."))
|
||||
value = models.CharField(max_length=255, help_text=_("Value of this user attribute."))
|
||||
|
||||
def __unicode__(self):
|
||||
"""Unicode representation of this attribute. """
|
||||
return u"[{username}] {name}: {value}".format(
|
||||
name=self.name,
|
||||
value=self.value,
|
||||
username=self.user.username,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def set_user_attribute(cls, user, name, value):
|
||||
"""
|
||||
Add an name/value pair as an attribute for the given
|
||||
user. Overwrites any previous value for that name, if it
|
||||
exists.
|
||||
"""
|
||||
cls.objects.filter(user=user, name=name).delete()
|
||||
cls.objects.create(user=user, name=name, value=value)
|
||||
|
||||
@classmethod
|
||||
def get_user_attribute(cls, user, name):
|
||||
"""
|
||||
Return the attribute value for the given user and name. If no such
|
||||
value exists, returns None.
|
||||
"""
|
||||
try:
|
||||
return cls.objects.get(user=user, name=name).value
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
@@ -19,6 +19,7 @@ from notification_prefs import NOTIFICATION_PREF_KEY
|
||||
from edxmako.tests import mako_middleware_process_request
|
||||
from external_auth.models import ExternalAuthMap
|
||||
import student
|
||||
from student.models import UserAttribute
|
||||
|
||||
TEST_CS_URL = 'https://comments.service.test:123/'
|
||||
|
||||
@@ -278,6 +279,24 @@ class TestCreateAccount(TestCase):
|
||||
else:
|
||||
self.assertIsNone(preference)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_referral_attribution(self):
|
||||
"""
|
||||
Verify that a referral attribution is recorded if an affiliate
|
||||
cookie is present upon a new user's registration.
|
||||
"""
|
||||
affiliate_id = 'test-partner'
|
||||
self.client.cookies[settings.AFFILIATE_COOKIE_NAME] = affiliate_id
|
||||
user = self.create_account_and_fetch_profile().user
|
||||
self.assertEqual(UserAttribute.get_user_attribute(user, settings.AFFILIATE_COOKIE_NAME), affiliate_id)
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_no_referral(self):
|
||||
"""Verify that no referral is recorded when a cookie is not present."""
|
||||
self.assertIsNone(self.client.cookies.get(settings.AFFILIATE_COOKIE_NAME)) # pylint: disable=no-member
|
||||
user = self.create_account_and_fetch_profile().user
|
||||
self.assertIsNone(UserAttribute.get_user_attribute(user, settings.AFFILIATE_COOKIE_NAME))
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCreateAccountValidation(TestCase):
|
||||
|
||||
@@ -24,7 +24,7 @@ from django.test.client import Client
|
||||
from course_modes.models import CourseMode
|
||||
from student.models import (
|
||||
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment,
|
||||
unique_id_for_user, LinkedInAddToProfileConfiguration
|
||||
unique_id_for_user, LinkedInAddToProfileConfiguration, UserAttribute
|
||||
)
|
||||
from student.views import (
|
||||
process_survey_link,
|
||||
@@ -1157,3 +1157,26 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
|
||||
self.assertContains(response, 'This course is 1 of 3 courses in the', count)
|
||||
self.assertContains(response, self.program_name, count * 2)
|
||||
self.assertContains(response, 'View XSeries Details', count)
|
||||
|
||||
|
||||
class UserAttributeTests(TestCase):
|
||||
"""Tests for the UserAttribute model."""
|
||||
|
||||
def setUp(self):
|
||||
super(UserAttributeTests, self).setUp()
|
||||
self.user = UserFactory()
|
||||
self.name = 'test'
|
||||
self.value = 'test-value'
|
||||
|
||||
def test_get_set_attribute(self):
|
||||
self.assertIsNone(UserAttribute.get_user_attribute(self.user, self.name))
|
||||
UserAttribute.set_user_attribute(self.user, self.name, self.value)
|
||||
self.assertEqual(UserAttribute.get_user_attribute(self.user, self.name), self.value)
|
||||
new_value = 'new_value'
|
||||
UserAttribute.set_user_attribute(self.user, self.name, new_value)
|
||||
self.assertEqual(UserAttribute.get_user_attribute(self.user, self.name), new_value)
|
||||
|
||||
def test_unicode(self):
|
||||
UserAttribute.set_user_attribute(self.user, self.name, self.value)
|
||||
for field in (self.name, self.value, self.user.username):
|
||||
self.assertIn(field, unicode(UserAttribute.objects.get(user=self.user)))
|
||||
|
||||
@@ -111,7 +111,7 @@ from student.helpers import (
|
||||
DISABLE_UNENROLL_CERT_STATES,
|
||||
)
|
||||
from student.cookies import set_logged_in_cookies, delete_logged_in_cookies
|
||||
from student.models import anonymous_id_for_user
|
||||
from student.models import anonymous_id_for_user, UserAttribute
|
||||
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
|
||||
|
||||
from embargo import api as embargo_api
|
||||
@@ -1814,6 +1814,8 @@ def create_account_with_params(request, params):
|
||||
login(request, new_user)
|
||||
request.session.set_expiry(0)
|
||||
|
||||
_record_registration_attribution(request, new_user)
|
||||
|
||||
# TODO: there is no error checking here to see that the user actually logged in successfully,
|
||||
# and is not yet an active user.
|
||||
if new_user is not None:
|
||||
@@ -1854,6 +1856,16 @@ def _enroll_user_in_pending_courses(student):
|
||||
)
|
||||
|
||||
|
||||
def _record_registration_attribution(request, user):
|
||||
"""
|
||||
Attribute this user's registration to the referring affiliate, if
|
||||
applicable.
|
||||
"""
|
||||
affiliate_id = request.COOKIES.get(settings.AFFILIATE_COOKIE_NAME)
|
||||
if user is not None and affiliate_id is not None:
|
||||
UserAttribute.set_user_attribute(user, settings.AFFILIATE_COOKIE_NAME, affiliate_id)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def create_account(request, post_override=None):
|
||||
"""
|
||||
|
||||
@@ -812,3 +812,5 @@ API_ACCESS_FROM_EMAIL = ENV_TOKENS.get('API_ACCESS_FROM_EMAIL')
|
||||
|
||||
# Mobile App Version Upgrade config
|
||||
APP_UPGRADE_CACHE_TIMEOUT = ENV_TOKENS.get('APP_UPGRADE_CACHE_TIMEOUT', APP_UPGRADE_CACHE_TIMEOUT)
|
||||
|
||||
AFFILIATE_COOKIE_NAME = ENV_TOKENS.get('AFFILIATE_COOKIE_NAME', AFFILIATE_COOKIE_NAME)
|
||||
|
||||
@@ -2892,3 +2892,6 @@ API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
|
||||
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
|
||||
API_DOCUMENTATION_URL = 'http://edx.readthedocs.org/projects/edx-platform-api/en/latest/overview.html'
|
||||
AUTH_DOCUMENTATION_URL = 'http://edx.readthedocs.org/projects/edx-platform-api/en/latest/authentication.html'
|
||||
|
||||
# Affiliate cookie tracking
|
||||
AFFILIATE_COOKIE_NAME = 'affiliate_id'
|
||||
|
||||
Reference in New Issue
Block a user