Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
a776733d
Commit
a776733d
authored
Oct 07, 2016
by
Bill DeRusha
Committed by
GitHub
Oct 07, 2016
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #13638 from edx/bderusha/utm-tracking
Add UTM tracking for registrations
parents
ed7f32ee
9877c6ad
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
176 additions
and
7 deletions
+176
-7
common/djangoapps/student/admin.py
+3
-1
common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py
+32
-0
common/djangoapps/student/models.py
+22
-0
common/djangoapps/student/tests/test_create_account.py
+65
-2
common/djangoapps/student/views.py
+54
-4
No files found.
common/djangoapps/student/admin.py
View file @
a776733d
...
...
@@ -11,7 +11,8 @@ from xmodule.modulestore.django import modulestore
from
config_models.admin
import
ConfigurationModelAdmin
from
student.models
import
(
UserProfile
,
UserTestGroup
,
CourseEnrollmentAllowed
,
DashboardConfiguration
,
CourseEnrollment
,
Registration
,
PendingNameChange
,
CourseAccessRole
,
LinkedInAddToProfileConfiguration
,
UserAttribute
,
LogoutViewConfiguration
PendingNameChange
,
CourseAccessRole
,
LinkedInAddToProfileConfiguration
,
UserAttribute
,
LogoutViewConfiguration
,
RegistrationCookieConfiguration
)
from
student.roles
import
REGISTERED_ACCESS_ROLES
...
...
@@ -184,6 +185,7 @@ admin.site.register(Registration)
admin
.
site
.
register
(
PendingNameChange
)
admin
.
site
.
register
(
DashboardConfiguration
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
LogoutViewConfiguration
,
ConfigurationModelAdmin
)
admin
.
site
.
register
(
RegistrationCookieConfiguration
,
ConfigurationModelAdmin
)
# We must first un-register the User model since it may also be registered by the auth app.
...
...
common/djangoapps/student/migrations/0007_registrationcookieconfiguration.py
0 → 100644
View file @
a776733d
# -*- coding: utf-8 -*-
from
__future__
import
unicode_literals
from
django.db
import
migrations
,
models
import
django.db.models.deletion
from
django.conf
import
settings
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
migrations
.
swappable_dependency
(
settings
.
AUTH_USER_MODEL
),
(
'student'
,
'0006_logoutviewconfiguration'
),
]
operations
=
[
migrations
.
CreateModel
(
name
=
'RegistrationCookieConfiguration'
,
fields
=
[
(
'id'
,
models
.
AutoField
(
verbose_name
=
'ID'
,
serialize
=
False
,
auto_created
=
True
,
primary_key
=
True
)),
(
'change_date'
,
models
.
DateTimeField
(
auto_now_add
=
True
,
verbose_name
=
'Change date'
)),
(
'enabled'
,
models
.
BooleanField
(
default
=
False
,
verbose_name
=
'Enabled'
)),
(
'utm_cookie_name'
,
models
.
CharField
(
help_text
=
'Name of the UTM cookie'
,
max_length
=
255
)),
(
'affiliate_cookie_name'
,
models
.
CharField
(
help_text
=
'Name of the affiliate cookie'
,
max_length
=
255
)),
(
'changed_by'
,
models
.
ForeignKey
(
on_delete
=
django
.
db
.
models
.
deletion
.
PROTECT
,
editable
=
False
,
to
=
settings
.
AUTH_USER_MODEL
,
null
=
True
,
verbose_name
=
'Changed by'
)),
],
options
=
{
'ordering'
:
(
'-change_date'
,),
'abstract'
:
False
,
},
),
]
common/djangoapps/student/models.py
View file @
a776733d
...
...
@@ -2250,6 +2250,28 @@ class EnrollmentRefundConfiguration(ConfigurationModel):
self
.
refund_window_microseconds
=
int
(
refund_window
.
total_seconds
()
*
1000000
)
class
RegistrationCookieConfiguration
(
ConfigurationModel
):
"""
Configuration for registration cookies.
"""
utm_cookie_name
=
models
.
CharField
(
max_length
=
255
,
help_text
=
_
(
"Name of the UTM cookie"
)
)
affiliate_cookie_name
=
models
.
CharField
(
max_length
=
255
,
help_text
=
_
(
"Name of the affiliate cookie"
)
)
def
__unicode__
(
self
):
"""Unicode representation of this config. """
return
u"UTM: {utm_name}; AFFILIATE: {affiliate_name}"
.
format
(
utm_name
=
self
.
utm_cookie_name
,
affiliate_name
=
self
.
affiliate_cookie_name
)
class
UserAttribute
(
TimeStampedModel
):
"""
Record additional metadata about a user, stored as key/value pairs of text.
...
...
common/djangoapps/student/tests/test_create_account.py
View file @
a776733d
...
...
@@ -12,6 +12,7 @@ from django.test.client import RequestFactory
from
django.test.utils
import
override_settings
from
django.utils.importlib
import
import_module
import
mock
import
pytz
from
openedx.core.djangoapps.user_api.preferences.api
import
get_user_preference
from
lang_pref
import
LANGUAGE_KEY
...
...
@@ -19,7 +20,7 @@ from notification_prefs import NOTIFICATION_PREF_KEY
from
openedx.core.djangoapps.external_auth.models
import
ExternalAuthMap
import
student
from
student.models
import
UserAttribute
from
student.views
import
REGISTRATION_AFFILIATE_ID
from
student.views
import
REGISTRATION_AFFILIATE_ID
,
REGISTRATION_UTM_PARAMETERS
,
REGISTRATION_UTM_CREATED_AT
TEST_CS_URL
=
'https://comments.service.test:123/'
...
...
@@ -280,7 +281,7 @@ class TestCreateAccount(TestCase):
self
.
assertIsNone
(
preference
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
def
test_referral_attribution
(
self
):
def
test_
affiliate_
referral_attribution
(
self
):
"""
Verify that a referral attribution is recorded if an affiliate
cookie is present upon a new user's registration.
...
...
@@ -291,11 +292,73 @@ class TestCreateAccount(TestCase):
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_AFFILIATE_ID
),
affiliate_id
)
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
def
test_utm_referral_attribution
(
self
):
"""
Verify that a referral attribution is recorded if an affiliate
cookie is present upon a new user's registration.
"""
utm_cookie_name
=
'edx.test.utm'
with
mock
.
patch
(
'student.models.RegistrationCookieConfiguration.current'
)
as
config
:
instance
=
config
.
return_value
instance
.
utm_cookie_name
=
utm_cookie_name
timestamp
=
1475521816879
utm_cookie
=
{
'utm_source'
:
'test-source'
,
'utm_medium'
:
'test-medium'
,
'utm_campaign'
:
'test-campaign'
,
'utm_term'
:
'test-term'
,
'utm_content'
:
'test-content'
,
'created_at'
:
timestamp
}
created_at
=
datetime
.
fromtimestamp
(
timestamp
/
float
(
1000
),
tz
=
pytz
.
UTC
)
self
.
client
.
cookies
[
utm_cookie_name
]
=
json
.
dumps
(
utm_cookie
)
user
=
self
.
create_account_and_fetch_profile
()
.
user
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_source'
)),
utm_cookie
.
get
(
'utm_source'
)
)
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_medium'
)),
utm_cookie
.
get
(
'utm_medium'
)
)
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_campaign'
)),
utm_cookie
.
get
(
'utm_campaign'
)
)
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_term'
)),
utm_cookie
.
get
(
'utm_term'
)
)
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_content'
)),
utm_cookie
.
get
(
'utm_content'
)
)
self
.
assertEqual
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_CREATED_AT
),
str
(
created_at
)
)
@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."""
utm_cookie_name
=
'edx.test.utm'
with
mock
.
patch
(
'student.models.RegistrationCookieConfiguration.current'
)
as
config
:
instance
=
config
.
return_value
instance
.
utm_cookie_name
=
utm_cookie_name
self
.
assertIsNone
(
self
.
client
.
cookies
.
get
(
settings
.
AFFILIATE_COOKIE_NAME
))
# pylint: disable=no-member
self
.
assertIsNone
(
self
.
client
.
cookies
.
get
(
utm_cookie_name
))
# pylint: disable=no-member
user
=
self
.
create_account_and_fetch_profile
()
.
user
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_AFFILIATE_ID
))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_source'
)))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_medium'
)))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_campaign'
)))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_term'
)))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
'utm_content'
)))
self
.
assertIsNone
(
UserAttribute
.
get_user_attribute
(
user
,
REGISTRATION_UTM_CREATED_AT
))
@ddt.ddt
...
...
common/djangoapps/student/views.py
View file @
a776733d
...
...
@@ -54,7 +54,7 @@ from student.models import (
CourseEnrollmentAllowed
,
UserStanding
,
LoginFailures
,
create_comments_service_user
,
PasswordHistory
,
UserSignupSource
,
DashboardConfiguration
,
LinkedInAddToProfileConfiguration
,
ManualEnrollmentAudit
,
ALLOWEDTOENROLL_TO_ENROLLED
,
LogoutViewConfiguration
)
LogoutViewConfiguration
,
RegistrationCookieConfiguration
)
from
student.forms
import
AccountCreationForm
,
PasswordResetFormNoActive
,
get_registration_extension_form
from
lms.djangoapps.commerce.utils
import
EcommerceService
# pylint: disable=import-error
from
lms.djangoapps.verify_student.models
import
SoftwareSecurePhotoVerification
# pylint: disable=import-error
...
...
@@ -133,6 +133,14 @@ ReverifyInfo = namedtuple('ReverifyInfo', 'course_id course_name course_number d
SETTING_CHANGE_INITIATED
=
'edx.user.settings.change_initiated'
# Used as the name of the user attribute for tracking affiliate registrations
REGISTRATION_AFFILIATE_ID
=
'registration_affiliate_id'
REGISTRATION_UTM_PARAMETERS
=
{
'utm_source'
:
'registration_utm_source'
,
'utm_medium'
:
'registration_utm_medium'
,
'utm_campaign'
:
'registration_utm_campaign'
,
'utm_term'
:
'registration_utm_term'
,
'utm_content'
:
'registration_utm_content'
,
}
REGISTRATION_UTM_CREATED_AT
=
'registration_utm_created_at'
# used to announce a registration
REGISTER_USER
=
Signal
(
providing_args
=
[
"user"
,
"profile"
])
...
...
@@ -1817,7 +1825,11 @@ def create_account_with_params(request, params):
login
(
request
,
new_user
)
request
.
session
.
set_expiry
(
0
)
_record_registration_attribution
(
request
,
new_user
)
try
:
record_registration_attributions
(
request
,
new_user
)
# Don't prevent a user from registering due to attribution errors.
except
Exception
:
# pylint: disable=broad-except
log
.
exception
(
'Error while attributing cookies to user registration.'
)
# TODO: there is no error checking here to see that the user actually logged in successfully,
# and is not yet an active user.
...
...
@@ -1859,16 +1871,54 @@ def _enroll_user_in_pending_courses(student):
)
def
_record
_registration_attribution
(
request
,
user
):
def
record_affiliate
_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
:
if
user
and
affiliate_id
:
UserAttribute
.
set_user_attribute
(
user
,
REGISTRATION_AFFILIATE_ID
,
affiliate_id
)
def
record_utm_registration_attribution
(
request
,
user
):
"""
Attribute this user's registration to the latest UTM referrer, if
applicable.
"""
utm_cookie_name
=
RegistrationCookieConfiguration
.
current
()
.
utm_cookie_name
utm_cookie
=
request
.
COOKIES
.
get
(
utm_cookie_name
)
if
user
and
utm_cookie
:
utm
=
json
.
loads
(
utm_cookie
)
for
utm_parameter
in
REGISTRATION_UTM_PARAMETERS
:
UserAttribute
.
set_user_attribute
(
user
,
REGISTRATION_UTM_PARAMETERS
.
get
(
utm_parameter
),
utm
.
get
(
utm_parameter
)
)
created_at_unixtime
=
utm
.
get
(
'created_at'
)
if
created_at_unixtime
:
# We divide by 1000 here because the javascript timestamp generated is in milliseconds not seconds.
# PYTHON: time.time() => 1475590280.823698
# JS: new Date().getTime() => 1475590280823
created_at_datetime
=
datetime
.
datetime
.
fromtimestamp
(
int
(
created_at_unixtime
)
/
float
(
1000
),
tz
=
UTC
)
else
:
created_at_datetime
=
None
UserAttribute
.
set_user_attribute
(
user
,
REGISTRATION_UTM_CREATED_AT
,
created_at_datetime
)
def
record_registration_attributions
(
request
,
user
):
"""
Attribute this user's registration based on referrer cookies.
"""
record_affiliate_registration_attribution
(
request
,
user
)
record_utm_registration_attribution
(
request
,
user
)
@csrf_exempt
def
create_account
(
request
,
post_override
=
None
):
"""
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment