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
1ebf9cf8
Unverified
Commit
1ebf9cf8
authored
Nov 02, 2017
by
Uzair Rasheed
Committed by
GitHub
Nov 02, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #16229 from edx/celery-task
celery task to update sailthru purchase record
parents
d4af6ec0
1b6ed3ba
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
322 additions
and
7 deletions
+322
-7
lms/djangoapps/email_marketing/signals.py
+26
-1
lms/djangoapps/email_marketing/tasks.py
+188
-0
lms/djangoapps/email_marketing/tests/test_signals.py
+108
-6
No files found.
lms/djangoapps/email_marketing/signals.py
View file @
1ebf9cf8
...
...
@@ -7,16 +7,19 @@ import logging
import
crum
from
django.conf
import
settings
from
django.dispatch
import
receiver
from
sailthru.sailthru_client
import
SailthruClient
from
sailthru.sailthru_error
import
SailthruClientError
from
celery.exceptions
import
TimeoutError
from
course_modes.models
import
CourseMode
from
email_marketing.models
import
EmailMarketingConfiguration
from
openedx.core.djangoapps.waffle_utils
import
WaffleSwitchNamespace
from
lms.djangoapps.email_marketing.tasks
import
update_user
,
update_user_email
,
get_email_cookies_via_sailthru
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
student.cookies
import
CREATE_LOGON_COOKIE
from
student.signals
import
ENROLL_STATUS_CHANGE
from
student.views
import
REGISTER_USER
from
util.model_utils
import
USER_FIELD_CHANGED
from
.tasks
import
update_course_enrollment
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -25,6 +28,28 @@ CHANGED_FIELDNAMES = ['username', 'is_active', 'name', 'gender', 'education',
'age'
,
'level_of_education'
,
'year_of_birth'
,
'country'
,
LANGUAGE_KEY
]
WAFFLE_NAMESPACE
=
'sailthru'
WAFFLE_SWITCHES
=
WaffleSwitchNamespace
(
name
=
WAFFLE_NAMESPACE
)
SAILTHRU_AUDIT_PURCHASE_ENABLED
=
'audit_purchase_enabled'
@receiver
(
ENROLL_STATUS_CHANGE
)
def
update_sailthru
(
sender
,
event
,
user
,
mode
,
course_id
,
**
kwargs
):
"""
Receives signal and calls a celery task to update the
enrollment track
Arguments:
user: current user
course_id: course key of a course
Returns:
None
"""
if
WAFFLE_SWITCHES
.
is_enabled
(
SAILTHRU_AUDIT_PURCHASE_ENABLED
)
and
mode
in
CourseMode
.
AUDIT_MODES
:
course_key
=
str
(
course_id
)
email
=
str
(
user
.
email
)
update_course_enrollment
.
delay
(
email
,
course_key
,
mode
)
@receiver
(
CREATE_LOGON_COOKIE
)
def
add_email_marketing_cookies
(
sender
,
response
=
None
,
user
=
None
,
...
...
lms/djangoapps/email_marketing/tasks.py
View file @
1ebf9cf8
...
...
@@ -291,3 +291,191 @@ def _retryable_sailthru_error(error):
"""
code
=
error
.
get_error_code
()
return
code
==
9
or
code
==
43
@task
(
bind
=
True
)
def
update_course_enrollment
(
self
,
email
,
course_key
,
mode
):
"""Adds/updates Sailthru when a user adds to cart/purchases/upgrades a course
Args:
user: current user
course_key: course key of course
Returns:
None
"""
course_url
=
build_course_url
(
course_key
)
config
=
EmailMarketingConfiguration
.
current
()
try
:
sailthru_client
=
SailthruClient
(
config
.
sailthru_key
,
config
.
sailthru_secret
)
except
:
return
send_template
=
config
.
sailthru_enroll_template
cost_in_cents
=
0
if
not
update_unenrolled_list
(
sailthru_client
,
email
,
course_url
,
False
):
schedule_retry
(
self
,
config
)
course_data
=
_get_course_content
(
course_key
,
course_url
,
sailthru_client
,
config
)
item
=
_build_purchase_item
(
course_key
,
course_url
,
cost_in_cents
,
mode
,
course_data
,
None
)
options
=
{}
if
send_template
:
options
[
'send_template'
]
=
send_template
if
not
_record_purchase
(
sailthru_client
,
email
,
item
,
options
):
schedule_retry
(
self
,
config
)
def
build_course_url
(
course_key
):
"""
Generates and return url of the course info page by using course_key
Arguments:
course_key: course_key of the given course
Returns
a complete url of the course info page
"""
return
'{base_url}/courses/{course_key}/info'
.
format
(
base_url
=
settings
.
LMS_ROOT_URL
,
course_key
=
unicode
(
course_key
))
def
update_unenrolled_list
(
sailthru_client
,
email
,
course_url
,
unenroll
):
"""Maintain a list of courses the user has unenrolled from in the Sailthru user record
Arguments:
sailthru_client: SailthruClient
email (str): user's email address
course_url (str): LMS url for course info page.
unenroll (boolean): True if unenrolling, False if enrolling
Returns:
False if retryable error, else True
"""
try
:
# get the user 'vars' values from sailthru
sailthru_response
=
sailthru_client
.
api_get
(
"user"
,
{
"id"
:
email
,
"fields"
:
{
"vars"
:
1
}})
if
not
sailthru_response
.
is_ok
():
error
=
sailthru_response
.
get_error
()
log
.
error
(
"Error attempting to read user record from Sailthru:
%
s"
,
error
.
get_message
())
return
not
_retryable_sailthru_error
(
error
)
response_json
=
sailthru_response
.
json
unenroll_list
=
[]
if
response_json
and
"vars"
in
response_json
and
response_json
[
"vars"
]
\
and
"unenrolled"
in
response_json
[
"vars"
]:
unenroll_list
=
response_json
[
"vars"
][
"unenrolled"
]
changed
=
False
# if unenrolling, add course to unenroll list
if
unenroll
:
if
course_url
not
in
unenroll_list
:
unenroll_list
.
append
(
course_url
)
changed
=
True
# if enrolling, remove course from unenroll list
elif
course_url
in
unenroll_list
:
unenroll_list
.
remove
(
course_url
)
changed
=
True
if
changed
:
# write user record back
sailthru_response
=
sailthru_client
.
api_post
(
'user'
,
{
'id'
:
email
,
'key'
:
'email'
,
'vars'
:
{
'unenrolled'
:
unenroll_list
}})
if
not
sailthru_response
.
is_ok
():
error
=
sailthru_response
.
get_error
()
log
.
error
(
"Error attempting to update user record in Sailthru:
%
s"
,
error
.
get_message
())
return
not
_retryable_sailthru_error
(
error
)
return
True
except
SailthruClientError
as
exc
:
log
.
exception
(
"Exception attempting to update user record for
%
s in Sailthru -
%
s"
,
email
,
unicode
(
exc
))
return
False
def
schedule_retry
(
self
,
config
):
"""Schedule a retry"""
raise
self
.
retry
(
countdown
=
config
.
sailthru_retry_interval
,
max_retries
=
config
.
sailthru_max_retries
)
def
_get_course_content
(
course_id
,
course_url
,
sailthru_client
,
config
):
"""Get course information using the Sailthru content api or from cache.
If there is an error, just return with an empty response.
Arguments:
course_id (str): course key of the course
course_url (str): LMS url for course info page.
sailthru_client : SailthruClient
config : config options
Returns:
course information from Sailthru
"""
# check cache first
cache_key
=
"{}:{}"
.
format
(
course_id
,
course_url
)
response
=
cache
.
get
(
cache_key
)
if
not
response
:
try
:
sailthru_response
=
sailthru_client
.
api_get
(
"content"
,
{
"id"
:
course_url
})
if
not
sailthru_response
.
is_ok
():
log
.
error
(
'Could not get course data from Sailthru on enroll/unenroll event. '
)
response
=
{}
else
:
response
=
sailthru_response
.
json
cache
.
set
(
cache_key
,
response
,
config
.
sailthru_content_cache_age
)
except
SailthruClientError
:
response
=
{}
return
response
def
_build_purchase_item
(
course_id
,
course_url
,
cost_in_cents
,
mode
,
course_data
,
sku
):
"""Build and return Sailthru purchase item object"""
# build item description
item
=
{
'id'
:
"{}-{}"
.
format
(
course_id
,
mode
),
'url'
:
course_url
,
'price'
:
cost_in_cents
,
'qty'
:
1
,
}
# get title from course info if we don't already have it from Sailthru
if
'title'
in
course_data
:
item
[
'title'
]
=
course_data
[
'title'
]
else
:
# can't find, just invent title
item
[
'title'
]
=
'Course {} mode: {}'
.
format
(
course_id
,
mode
)
if
'tags'
in
course_data
:
item
[
'tags'
]
=
course_data
[
'tags'
]
return
item
def
_record_purchase
(
sailthru_client
,
email
,
item
,
options
):
"""
Record a purchase in Sailthru
Arguments:
sailthru_client: SailthruClient
email: user's email address
item: Sailthru required information
options: Sailthru purchase API options
Returns:
False if retryable error, else True
"""
try
:
sailthru_response
=
sailthru_client
.
purchase
(
email
,
[
item
],
options
=
options
)
if
not
sailthru_response
.
is_ok
():
error
=
sailthru_response
.
get_error
()
log
.
error
(
"Error attempting to record purchase in Sailthru:
%
s"
,
error
.
get_message
())
return
not
_retryable_sailthru_error
(
error
)
except
SailthruClientError
as
exc
:
log
.
exception
(
"Exception attempting to record purchase for
%
s in Sailthru -
%
s"
,
email
,
unicode
(
exc
))
return
False
return
True
lms/djangoapps/email_marketing/tests/test_signals.py
View file @
1ebf9cf8
...
...
@@ -19,7 +19,8 @@ from email_marketing.models import EmailMarketingConfiguration
from
email_marketing.signals
import
(
add_email_marketing_cookies
,
email_marketing_register_user
,
email_marketing_user_field_changed
email_marketing_user_field_changed
,
update_sailthru
)
from
email_marketing.tasks
import
(
_create_user_list
,
...
...
@@ -27,11 +28,12 @@ from email_marketing.tasks import (
_get_or_create_user_list
,
update_user
,
update_user_email
,
get_email_cookies_via_sailthru
get_email_cookies_via_sailthru
,
update_course_enrollment
,
)
from
openedx.core.djangoapps.lang_pref
import
LANGUAGE_KEY
from
student.models
import
Registration
from
student.tests.factories
import
UserFactory
,
UserProfileFactory
from
student.tests.factories
import
UserFactory
,
UserProfileFactory
,
CourseEnrollmentFactory
from
util.json_request
import
JsonResponse
log
=
logging
.
getLogger
(
__name__
)
...
...
@@ -89,7 +91,7 @@ class EmailMarketingTests(TestCase):
@freeze_time
(
datetime
.
datetime
.
now
())
@patch
(
'email_marketing.signals.crum.get_current_request'
)
@patch
(
'
email_marketing.signals
.SailthruClient.api_post'
)
@patch
(
'
sailthru.sailthru_client
.SailthruClient.api_post'
)
def
test_drop_cookie
(
self
,
mock_sailthru
,
mock_get_current_request
):
"""
Test add_email_marketing_cookies
...
...
@@ -127,7 +129,7 @@ class EmailMarketingTests(TestCase):
self
.
assertTrue
(
'sailthru_hid'
in
response
.
cookies
)
self
.
assertEquals
(
response
.
cookies
[
'sailthru_hid'
]
.
value
,
"test_cookie"
)
@patch
(
'
email_marketing.signals
.SailthruClient.api_post'
)
@patch
(
'
sailthru.sailthru_client
.SailthruClient.api_post'
)
def
test_get_cookies_via_sailthu
(
self
,
mock_sailthru
):
cookies
=
{
'cookie'
:
'test_cookie'
}
...
...
@@ -149,7 +151,7 @@ class EmailMarketingTests(TestCase):
self
.
assertEqual
(
cookies
[
'cookie'
],
expected_cookie
.
result
)
@patch
(
'
email_marketing.signals
.SailthruClient.api_post'
)
@patch
(
'
sailthru.sailthru_client
.SailthruClient.api_post'
)
def
test_drop_cookie_error_path
(
self
,
mock_sailthru
):
"""
test that error paths return no cookie
...
...
@@ -523,3 +525,103 @@ class EmailMarketingTests(TestCase):
update_email_marketing_config
(
enabled
=
False
)
email_marketing_user_field_changed
(
None
,
self
.
user
,
table
=
'auth_user'
,
setting
=
'email'
,
old_value
=
'new@a.com'
)
self
.
assertFalse
(
mock_update_user
.
called
)
class
MockSailthruResponse
(
object
):
"""
Mock object for SailthruResponse
"""
def
__init__
(
self
,
json_response
,
error
=
None
,
code
=
1
):
self
.
json
=
json_response
self
.
error
=
error
self
.
code
=
code
def
is_ok
(
self
):
"""
Return true of no error
"""
return
self
.
error
is
None
def
get_error
(
self
):
"""
Get error description
"""
return
MockSailthruError
(
self
.
error
,
self
.
code
)
class
MockSailthruError
(
object
):
"""
Mock object for Sailthru Error
"""
def
__init__
(
self
,
error
,
code
=
1
):
self
.
error
=
error
self
.
code
=
code
def
get_message
(
self
):
"""
Get error description
"""
return
self
.
error
def
get_error_code
(
self
):
"""
Get error code
"""
return
self
.
code
class
SailthruTests
(
TestCase
):
"""
Tests for the Sailthru tasks class.
"""
def
setUp
(
self
):
super
(
SailthruTests
,
self
)
.
setUp
()
self
.
user
=
UserFactory
()
self
.
course_id
=
CourseKey
.
from_string
(
'edX/toy/2012_Fall'
)
self
.
course_url
=
'http://lms.testserver.fake/courses/edX/toy/2012_Fall/info'
self
.
course_id2
=
'edX/toy/2016_Fall'
self
.
course_url2
=
'http://lms.testserver.fake/courses/edX/toy/2016_Fall/info'
@patch
(
'sailthru.sailthru_client.SailthruClient.purchase'
)
@patch
(
'sailthru.sailthru_client.SailthruClient.api_get'
)
@patch
(
'sailthru.sailthru_client.SailthruClient.api_post'
)
def
test_update_course_enrollment
(
self
,
mock_sailthru_api_post
,
mock_sailthru_api_get
,
mock_sailthru_purchase
):
"""test update sailthru user record"""
# create mocked Sailthru API responses
mock_sailthru_api_post
.
return_value
=
MockSailthruResponse
({
'ok'
:
True
})
mock_sailthru_api_get
.
return_value
=
MockSailthruResponse
({
'user'
:
{
"id"
:
TEST_EMAIL
,
"fields"
:
{
"vars"
:
1
}}})
mock_sailthru_purchase
.
return_value
=
MockSailthruResponse
({
'ok'
:
True
})
self
.
user
.
email
=
TEST_EMAIL
CourseEnrollmentFactory
(
user
=
self
.
user
,
course_id
=
self
.
course_id
)
with
patch
(
'email_marketing.tasks.build_course_url'
)
as
m
:
m
.
return_value
=
self
.
course_url
update_course_enrollment
(
TEST_EMAIL
,
self
.
course_id
,
'audit'
)
item
=
[{
'url'
:
self
.
course_url
,
'price'
:
0
,
'qty'
:
1
,
'id'
:
'edX/toy/2012_Fall-audit'
,
'title'
:
'Course edX/toy/2012_Fall mode: audit'
}]
mock_sailthru_purchase
.
assert_called_with
(
TEST_EMAIL
,
item
,
options
=
{})
@patch
(
'sailthru.sailthru_client.SailthruClient.purchase'
)
def
test_switch_is_disabled
(
self
,
mock_sailthru_purchase
):
"""Make sure sailthru purchase is not called when waffle switch is disabled"""
update_sailthru
(
None
,
None
,
self
.
user
,
'verified'
,
self
.
course_id
)
self
.
assertFalse
(
mock_sailthru_purchase
.
called
)
@patch
(
'openedx.core.djangoapps.waffle_utils.WaffleSwitchNamespace.is_enabled'
)
@patch
(
'sailthru.sailthru_client.SailthruClient.purchase'
)
def
test_purchase_is_not_invoked
(
self
,
mock_sailthru_purchase
,
switch
):
"""Make sure purchase is not called in the following condition:
i: waffle switch is True and mode is verified
"""
switch
.
return_value
=
True
update_sailthru
(
None
,
None
,
self
.
user
,
'verified'
,
self
.
course_id
)
self
.
assertFalse
(
mock_sailthru_purchase
.
called
)
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