Commit 0e66baf4 by Peter Fogg

Add referral tracking for new registrations.

ECOM-4325
parent 140fd85e
......@@ -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'
# -*- 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
......@@ -1815,6 +1815,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:
......@@ -1855,6 +1857,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)
......@@ -2900,3 +2900,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'
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