Commit aa078dfd by PaulWattenberger Committed by GitHub

Pwattenberger/sailthru enroll (#12816)

* Partial changes for purchase tracking

* Continued changes for purchase tracking

* Clean up code quality issues

* Clean up code quality issues

* Responses to code review

* Fix code quality flaged issues

* Fix code quality flaged issues

* Fix code quality flaged issues

* Fix problem processing sailthru_content cookie
parent 664cafc9
...@@ -61,10 +61,32 @@ from util.milestones_helpers import is_entrance_exams_enabled ...@@ -61,10 +61,32 @@ from util.milestones_helpers import is_entrance_exams_enabled
UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"]) UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
ENROLL_STATUS_CHANGE = Signal(providing_args=["event", "user", "course_id", "mode", "cost", "currency"])
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AUDIT_LOG = logging.getLogger("audit") AUDIT_LOG = logging.getLogger("audit")
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore # pylint: disable=invalid-name SessionStore = import_module(settings.SESSION_ENGINE).SessionStore # pylint: disable=invalid-name
# enroll status changed events - signaled to email_marketing. See email_marketing.tasks for more info
# ENROLL signal used for free enrollment only
class EnrollStatusChange(object):
"""
Possible event types for ENROLL_STATUS_CHANGE signal
"""
# enroll for a course
enroll = 'enroll'
# unenroll for a course
unenroll = 'unenroll'
# add an upgrade to cart
upgrade_start = 'upgrade_start'
# complete an upgrade purchase
upgrade_complete = 'upgrade_complete'
# add a paid course to the cart
paid_start = 'paid_start'
# complete a paid course purchase
paid_complete = 'paid_complete'
UNENROLLED_TO_ALLOWEDTOENROLL = 'from unenrolled to allowed to enroll' UNENROLLED_TO_ALLOWEDTOENROLL = 'from unenrolled to allowed to enroll'
ALLOWEDTOENROLL_TO_ENROLLED = 'from allowed to enroll to enrolled' ALLOWEDTOENROLL_TO_ENROLLED = 'from allowed to enroll to enrolled'
ENROLLED_TO_ENROLLED = 'from enrolled to enrolled' ENROLLED_TO_ENROLLED = 'from enrolled to enrolled'
...@@ -1113,6 +1135,7 @@ class CourseEnrollment(models.Model): ...@@ -1113,6 +1135,7 @@ class CourseEnrollment(models.Model):
UNENROLL_DONE.send(sender=None, course_enrollment=self, skip_refund=skip_refund) UNENROLL_DONE.send(sender=None, course_enrollment=self, skip_refund=skip_refund)
self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED) self.emit_event(EVENT_NAME_ENROLLMENT_DEACTIVATED)
self.send_signal(EnrollStatusChange.unenroll)
dog_stats_api.increment( dog_stats_api.increment(
"common.student.unenrollment", "common.student.unenrollment",
...@@ -1125,6 +1148,24 @@ class CourseEnrollment(models.Model): ...@@ -1125,6 +1148,24 @@ class CourseEnrollment(models.Model):
# mode has changed from its previous setting # mode has changed from its previous setting
self.emit_event(EVENT_NAME_ENROLLMENT_MODE_CHANGED) self.emit_event(EVENT_NAME_ENROLLMENT_MODE_CHANGED)
def send_signal(self, event, cost=None, currency=None):
"""
Sends a signal announcing changes in course enrollment status.
"""
ENROLL_STATUS_CHANGE.send(sender=None, event=event, user=self.user,
mode=self.mode, course_id=self.course_id,
cost=cost, currency=currency)
@classmethod
def send_signal_full(cls, event, user=user, mode=mode, course_id=course_id, cost=None, currency=None):
"""
Sends a signal announcing changes in course enrollment status.
This version should be used if you don't already have a CourseEnrollment object
"""
ENROLL_STATUS_CHANGE.send(sender=None, event=event, user=user,
mode=mode, course_id=course_id,
cost=cost, currency=currency)
def emit_event(self, event_name): def emit_event(self, event_name):
""" """
Emits an event to explicitly track course enrollment and unenrollment. Emits an event to explicitly track course enrollment and unenrollment.
......
...@@ -112,7 +112,7 @@ from student.helpers import ( ...@@ -112,7 +112,7 @@ from student.helpers import (
DISABLE_UNENROLL_CERT_STATES, DISABLE_UNENROLL_CERT_STATES,
) )
from student.cookies import set_logged_in_cookies, delete_logged_in_cookies from student.cookies import set_logged_in_cookies, delete_logged_in_cookies
from student.models import anonymous_id_for_user, UserAttribute from student.models import anonymous_id_for_user, UserAttribute, EnrollStatusChange
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
from embargo import api as embargo_api from embargo import api as embargo_api
...@@ -1065,7 +1065,8 @@ def change_enrollment(request, check_access=True): ...@@ -1065,7 +1065,8 @@ def change_enrollment(request, check_access=True):
try: try:
enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes)
if enroll_mode: if enroll_mode:
CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) enrollment = CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode)
enrollment.send_signal(EnrollStatusChange.enroll)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
return HttpResponseBadRequest(_("Could not enroll")) return HttpResponseBadRequest(_("Could not enroll"))
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('email_marketing', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_abandoned_cart_delay',
field=models.IntegerField(default=60, help_text='Sailthru minutes to wait before sending abandoned cart message.'),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_abandoned_cart_template',
field=models.CharField(help_text='Sailthru template to use on abandoned cart reminder. ', max_length=20, blank=True),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_content_cache_age',
field=models.IntegerField(default=3600, help_text='Number of seconds to cache course content retrieved from Sailthru.'),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_enroll_cost',
field=models.IntegerField(default=100, help_text='Cost in cents to report to Sailthru for enrolls.'),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_enroll_template',
field=models.CharField(help_text='Sailthru send template to use on enrolling for audit. ', max_length=20, blank=True),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_get_tags_from_sailthru',
field=models.BooleanField(default=True, help_text='Use the Sailthru content API to fetch course tags.'),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_purchase_template',
field=models.CharField(help_text='Sailthru send template to use on purchasing a course seat. ', max_length=20, blank=True),
),
migrations.AddField(
model_name='emailmarketingconfiguration',
name='sailthru_upgrade_template',
field=models.CharField(help_text='Sailthru send template to use on upgrading a course. ', max_length=20, blank=True),
),
migrations.AlterField(
model_name='emailmarketingconfiguration',
name='sailthru_activation_template',
field=models.CharField(help_text='Sailthru template to use on activation send. ', max_length=20, blank=True),
),
]
...@@ -50,11 +50,75 @@ class EmailMarketingConfiguration(ConfigurationModel): ...@@ -50,11 +50,75 @@ class EmailMarketingConfiguration(ConfigurationModel):
sailthru_activation_template = models.fields.CharField( sailthru_activation_template = models.fields.CharField(
max_length=20, max_length=20,
blank=True,
help_text=_( help_text=_(
"Sailthru template to use on activation send. " "Sailthru template to use on activation send. "
) )
) )
sailthru_abandoned_cart_template = models.fields.CharField(
max_length=20,
blank=True,
help_text=_(
"Sailthru template to use on abandoned cart reminder. "
)
)
sailthru_abandoned_cart_delay = models.fields.IntegerField(
default=60,
help_text=_(
"Sailthru minutes to wait before sending abandoned cart message."
)
)
sailthru_enroll_template = models.fields.CharField(
max_length=20,
blank=True,
help_text=_(
"Sailthru send template to use on enrolling for audit. "
)
)
sailthru_upgrade_template = models.fields.CharField(
max_length=20,
blank=True,
help_text=_(
"Sailthru send template to use on upgrading a course. "
)
)
sailthru_purchase_template = models.fields.CharField(
max_length=20,
blank=True,
help_text=_(
"Sailthru send template to use on purchasing a course seat. "
)
)
# Sailthru purchases can be tagged with interest tags to provide information about the types of courses
# users are interested in. The easiest way to get the tags currently is the Sailthru content API which
# looks in the content library (the content library is populated daily with a script that pulls the data
# from the course discovery API) This option should normally be on, but it does add overhead to processing
# purchases and enrolls.
sailthru_get_tags_from_sailthru = models.BooleanField(
default=True,
help_text=_('Use the Sailthru content API to fetch course tags.')
)
sailthru_content_cache_age = models.fields.IntegerField(
default=3600,
help_text=_(
"Number of seconds to cache course content retrieved from Sailthru."
)
)
sailthru_enroll_cost = models.fields.IntegerField(
default=100,
help_text=_(
"Cost in cents to report to Sailthru for enrolls."
)
)
def __unicode__(self): def __unicode__(self):
return u"Email marketing configuration: New user list %s, Activation template: %s" % \ return u"Email marketing configuration: New user list %s, Activation template: %s" % \
(self.sailthru_new_user_list, self.sailthru_activation_template) (self.sailthru_new_user_list, self.sailthru_activation_template)
...@@ -3,15 +3,16 @@ This module contains signals needed for email integration ...@@ -3,15 +3,16 @@ This module contains signals needed for email integration
""" """
import logging import logging
import datetime import datetime
import crum
from django.dispatch import receiver from django.dispatch import receiver
from student.models import UNENROLL_DONE from student.models import ENROLL_STATUS_CHANGE
from student.cookies import CREATE_LOGON_COOKIE from student.cookies import CREATE_LOGON_COOKIE
from student.views import REGISTER_USER from student.views import REGISTER_USER
from email_marketing.models import EmailMarketingConfiguration from email_marketing.models import EmailMarketingConfiguration
from util.model_utils import USER_FIELD_CHANGED from util.model_utils import USER_FIELD_CHANGED
from lms.djangoapps.email_marketing.tasks import update_user, update_user_email from lms.djangoapps.email_marketing.tasks import update_user, update_user_email, update_course_enrollment
from sailthru.sailthru_client import SailthruClient from sailthru.sailthru_client import SailthruClient
from sailthru.sailthru_error import SailthruClientError from sailthru.sailthru_error import SailthruClientError
...@@ -24,17 +25,45 @@ CHANGED_FIELDNAMES = ['username', 'is_active', 'name', 'gender', 'education', ...@@ -24,17 +25,45 @@ CHANGED_FIELDNAMES = ['username', 'is_active', 'name', 'gender', 'education',
'country'] 'country']
@receiver(UNENROLL_DONE) @receiver(ENROLL_STATUS_CHANGE)
def handle_unenroll_done(sender, course_enrollment=None, skip_refund=False, def handle_enroll_status_change(sender, event=None, user=None, mode=None, course_id=None, cost=None, currency=None,
**kwargs): # pylint: disable=unused-argument **kwargs): # pylint: disable=unused-argument
""" """
Signal receiver for unenrollments Signal receiver for enroll/unenroll/purchase events
""" """
email_config = EmailMarketingConfiguration.current() email_config = EmailMarketingConfiguration.current()
if not email_config.enabled: if not email_config.enabled or not event or not user or not mode or not course_id:
return
request = crum.get_current_request()
if not request:
return return
# TBD # figure out course url
course_url = _build_course_url(request, course_id.to_deprecated_string())
# pass event to email_marketing.tasks
update_course_enrollment.delay(user.email, course_url, event, mode,
unit_cost=cost, course_id=course_id, currency=currency,
message_id=request.COOKIES.get('sailthru_bid'))
def _build_course_url(request, course_id):
"""
Build a course url from a course id and the host from the current request
:param request:
:param course_id:
:return:
"""
host = request.get_host()
# hack for integration testing since Sailthru rejects urls with localhost
if host.startswith('localhost'):
host = 'courses.edx.org'
return '{scheme}://{host}/courses/{course}/info'.format(
scheme=request.scheme,
host=host,
course=course_id
)
@receiver(CREATE_LOGON_COOKIE) @receiver(CREATE_LOGON_COOKIE)
...@@ -54,12 +83,23 @@ def add_email_marketing_cookies(sender, response=None, user=None, ...@@ -54,12 +83,23 @@ def add_email_marketing_cookies(sender, response=None, user=None,
if not email_config.enabled: if not email_config.enabled:
return response return response
post_parms = {
'id': user.email,
'fields': {'keys': 1},
'vars': {'last_login_date': datetime.datetime.now().strftime("%Y-%m-%d")}
}
# get sailthru_content cookie to capture usage before logon
request = crum.get_current_request()
if request:
sailthru_content = request.COOKIES.get('sailthru_content')
if sailthru_content:
post_parms['cookies'] = {'sailthru_content': sailthru_content}
try: try:
sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret)
sailthru_response = \ sailthru_response = \
sailthru_client.api_post("user", {'id': user.email, 'fields': {'keys': 1}, sailthru_client.api_post("user", post_parms)
'vars': {'last_login_date':
datetime.datetime.now().strftime("%Y-%m-%d")}})
except SailthruClientError as exc: except SailthruClientError as exc:
log.error("Exception attempting to obtain cookie from Sailthru: %s", unicode(exc)) log.error("Exception attempting to obtain cookie from Sailthru: %s", unicode(exc))
return response return response
...@@ -93,7 +133,6 @@ def email_marketing_register_user(sender, user=None, profile=None, ...@@ -93,7 +133,6 @@ def email_marketing_register_user(sender, user=None, profile=None,
profile: The user profile for the user being changed profile: The user profile for the user being changed
kwargs: Not used kwargs: Not used
""" """
log.info("Receiving REGISTER_USER")
email_config = EmailMarketingConfiguration.current() email_config = EmailMarketingConfiguration.current()
if not email_config.enabled: if not email_config.enabled:
return return
......
...@@ -6,8 +6,13 @@ import time ...@@ -6,8 +6,13 @@ import time
from celery import task from celery import task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.http import Http404
from django.core.cache import cache
from email_marketing.models import EmailMarketingConfiguration from email_marketing.models import EmailMarketingConfiguration
from course_modes.models import CourseMode
from courseware.courses import get_course_by_id
from student.models import EnrollStatusChange
from sailthru.sailthru_client import SailthruClient from sailthru.sailthru_client import SailthruClient
from sailthru.sailthru_error import SailthruClientError from sailthru.sailthru_error import SailthruClientError
...@@ -30,16 +35,14 @@ def update_user(self, username, new_user=False, activation=False): ...@@ -30,16 +35,14 @@ def update_user(self, username, new_user=False, activation=False):
return return
# get user # get user
user = User.objects.select_related('profile').get(username=username) try:
if not user: user = User.objects.select_related('profile').get(username=username)
except User.DoesNotExist:
log.error("User not found during Sailthru update %s", username) log.error("User not found during Sailthru update %s", username)
return return
# get profile # get profile
profile = user.profile profile = user.profile
if not profile:
log.error("User profile not found during Sailthru update %s", username)
return
sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret)
try: try:
...@@ -92,8 +95,9 @@ def update_user_email(self, username, old_email): ...@@ -92,8 +95,9 @@ def update_user_email(self, username, old_email):
return return
# get user # get user
user = User.objects.get(username=username) try:
if not user: user = User.objects.get(username=username)
except User.DoesNotExist:
log.error("User not found duing Sailthru update %s", username) log.error("User not found duing Sailthru update %s", username)
return return
...@@ -145,3 +149,269 @@ def _create_sailthru_user_parm(user, profile, new_user, email_config): ...@@ -145,3 +149,269 @@ def _create_sailthru_user_parm(user, profile, new_user, email_config):
sailthru_user['lists'] = {email_config.sailthru_new_user_list: 1} sailthru_user['lists'] = {email_config.sailthru_new_user_list: 1}
return sailthru_user return sailthru_user
# pylint: disable=not-callable
@task(bind=True, default_retry_delay=3600, max_retries=24)
def update_course_enrollment(self, email, course_url, event, mode,
unit_cost=None, course_id=None,
currency=None, message_id=None): # pylint: disable=unused-argument
"""
Adds/updates Sailthru when a user enrolls/unenrolls/adds to cart/purchases/upgrades a course
Args:
email(str): The user's email address
course_url(str): Course home page url
event(str): event type
mode(object): enroll mode (audit, verification, ...)
unit_cost: cost if purchase event
course_id(CourseKey): course id
currency(str): currency if purchase event - currently ignored since Sailthru only supports USD
Returns:
None
The event can be one of the following:
EnrollStatusChange.enroll
A free enroll (mode=audit)
EnrollStatusChange.unenroll
An unenroll
EnrollStatusChange.upgrade_start
A paid upgrade added to cart
EnrollStatusChange.upgrade_complete
A paid upgrade purchase complete
EnrollStatusChange.paid_start
A non-free course added to cart
EnrollStatusChange.paid_complete
A non-free course purchase complete
"""
email_config = EmailMarketingConfiguration.current()
if not email_config.enabled:
return
course_id_string = course_id.to_deprecated_string()
# Use event type to figure out processing required
new_enroll = unenroll = fetch_tags = False
incomplete = send_template = None
if unit_cost:
cost_in_cents = unit_cost * 100
if event == EnrollStatusChange.enroll:
# new enroll for audit (no cost)
new_enroll = True
fetch_tags = True
send_template = email_config.sailthru_enroll_template
# set cost of $1 so that Sailthru recognizes the event
cost_in_cents = email_config.sailthru_enroll_cost
elif event == EnrollStatusChange.unenroll:
# unenroll - need to update list of unenrolled courses for user in Sailthru
unenroll = True
elif event == EnrollStatusChange.upgrade_start:
# add upgrade to cart
incomplete = 1
elif event == EnrollStatusChange.paid_start:
# add course purchase (probably 'honor') to cart
incomplete = 1
elif event == EnrollStatusChange.upgrade_complete:
# upgrade complete
fetch_tags = True
send_template = email_config.sailthru_upgrade_template
elif event == EnrollStatusChange.paid_complete:
# paid course purchase complete
new_enroll = True
fetch_tags = True
send_template = email_config.sailthru_purchase_template
sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret)
# update the "unenrolled" course array in the user record on Sailthru if new enroll or unenroll
if new_enroll or unenroll:
if not _update_unenrolled_list(sailthru_client, email, course_url, unenroll):
raise self.retry(countdown=email_config.sailthru_retry_interval,
max_retries=email_config.sailthru_max_retries)
# if there is a cost, call Sailthru purchase api to record
if cost_in_cents:
# get course information if configured and appropriate event
if fetch_tags and email_config.sailthru_get_tags_from_sailthru:
course_data = _get_course_content(course_url, sailthru_client, email_config)
else:
course_data = {}
# build item description
item = _build_purchase_item(course_id_string, course_url, cost_in_cents, mode, course_data, course_id)
# build purchase api options list
options = {}
if incomplete and email_config.sailthru_abandoned_cart_template:
options['reminder_template'] = email_config.sailthru_abandoned_cart_template
options['reminder_time'] = "+{} minutes".format(email_config.sailthru_abandoned_cart_delay)
# add appropriate send template
if send_template:
options['send_template'] = send_template
if not _record_purchase(sailthru_client, email, item, incomplete, message_id, options):
raise self.retry(countdown=email_config.sailthru_retry_interval,
max_retries=email_config.sailthru_max_retries)
def _build_purchase_item(course_id_string, course_url, cost_in_cents, mode, course_data, course_id):
"""
Build Sailthru purchase item object
:return: item
"""
# build item description
item = {
'id': "{}-{}".format(course_id_string, 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:
try:
course = get_course_by_id(course_id)
item['title'] = course.display_name
except Http404:
# can't find, just invent title
item['title'] = 'Course {} mode: {}'.format(course_id_string, mode)
if 'tags' in course_data:
item['tags'] = course_data['tags']
# add vars to item
sailthru_vars = {}
if 'vars' in course_data:
sailthru_vars = course_data['vars']
sailthru_vars['mode'] = mode
sailthru_vars['course_run_id'] = course_id_string
item['vars'] = sailthru_vars
# get list of modes for course and add upgrade deadlines for verified modes
for mode_entry in CourseMode.modes_for_course(course_id):
if mode_entry.expiration_datetime is not None and CourseMode.is_verified_slug(mode_entry.slug):
sailthru_vars['upgrade_deadline_{}'.format(mode_entry.slug)] = \
mode_entry.expiration_datetime.strftime("%Y-%m-%d")
return item
def _record_purchase(sailthru_client, email, item, incomplete, message_id, options):
"""
Record a purchase in Sailthru
:param sailthru_client:
:param email:
:param item:
:param incomplete:
:param message_id:
:param options:
:return: False it retryable error
"""
try:
sailthru_response = sailthru_client.purchase(email, [item],
incomplete=incomplete, message_id=message_id,
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 False
except SailthruClientError as exc:
log.error("Exception attempting to record purchase for %s in Sailthru - %s", email, unicode(exc))
return False
return True
def _get_course_content(course_url, sailthru_client, email_config):
"""
Get course information using the Sailthru content api.
If there is an error, just return with an empty response.
:param course_url:
:param sailthru_client:
:return: dict with course information
"""
# check cache first
response = cache.get(course_url)
if not response:
try:
sailthru_response = sailthru_client.api_get("content", {"id": course_url})
if not sailthru_response.is_ok():
return {}
response = sailthru_response.json
cache.set(course_url, response, email_config.sailthru_content_cache_age)
except SailthruClientError:
response = {}
return response
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
:param sailthru_client:
:param email:
:param email_config:
:param course_url:
:param unenroll:
:return: 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 False
response_json = sailthru_response.json
unenroll_list = []
if response_json and "vars" in response_json 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 False
# everything worked
return True
except SailthruClientError as exc:
log.error("Exception attempting to update user record for %s in Sailthru - %s", email, unicode(exc))
return False
"""Tests of email marketing signal handlers.""" """Tests of email marketing signal handlers."""
import logging import logging
import ddt import ddt
import datetime
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.contrib.auth.models import AnonymousUser
from mock import patch from mock import patch, ANY
from util.json_request import JsonResponse from util.json_request import JsonResponse
from django.http import Http404
from email_marketing.signals import handle_unenroll_done, \ from email_marketing.signals import handle_enroll_status_change, \
email_marketing_register_user, \ email_marketing_register_user, \
email_marketing_user_field_changed, \ email_marketing_user_field_changed, \
add_email_marketing_cookies add_email_marketing_cookies
from email_marketing.tasks import update_user, update_user_email from email_marketing.tasks import update_user, update_user_email, update_course_enrollment, \
_get_course_content, _update_unenrolled_list
from email_marketing.models import EmailMarketingConfiguration from email_marketing.models import EmailMarketingConfiguration
from django.test.client import RequestFactory from django.test.client import RequestFactory
from student.tests.factories import UserFactory, UserProfileFactory from student.tests.factories import UserFactory, UserProfileFactory
from request_cache.middleware import RequestCache
from student.models import EnrollStatusChange
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from xmodule.modulestore.tests.factories import CourseFactory
from sailthru.sailthru_client import SailthruClient from sailthru.sailthru_client import SailthruClient
from sailthru.sailthru_response import SailthruResponse from sailthru.sailthru_response import SailthruResponse
...@@ -35,7 +43,12 @@ def update_email_marketing_config(enabled=False, key='badkey', secret='badsecret ...@@ -35,7 +43,12 @@ def update_email_marketing_config(enabled=False, key='badkey', secret='badsecret
sailthru_key=key, sailthru_key=key,
sailthru_secret=secret, sailthru_secret=secret,
sailthru_new_user_list=new_user_list, sailthru_new_user_list=new_user_list,
sailthru_activation_template=template sailthru_activation_template=template,
sailthru_enroll_template='enroll_template',
sailthru_upgrade_template='upgrade_template',
sailthru_purchase_template='purchase_template',
sailthru_abandoned_cart_template='abandoned_template',
sailthru_get_tags_from_sailthru=False
) )
...@@ -51,10 +64,16 @@ class EmailMarketingTests(TestCase): ...@@ -51,10 +64,16 @@ class EmailMarketingTests(TestCase):
self.profile = self.user.profile self.profile = self.user.profile
self.request = self.request_factory.get("foo") self.request = self.request_factory.get("foo")
update_email_marketing_config(enabled=True) update_email_marketing_config(enabled=True)
# create some test course objects
self.course_id_string = 'edX/toy/2012_Fall'
self.course_id = CourseKey.from_string(self.course_id_string)
self.course_url = 'http://testserver/courses/edX/toy/2012_Fall/info'
super(EmailMarketingTests, self).setUp() super(EmailMarketingTests, self).setUp()
@patch('email_marketing.signals.crum.get_current_request')
@patch('email_marketing.signals.SailthruClient.api_post') @patch('email_marketing.signals.SailthruClient.api_post')
def test_drop_cookie(self, mock_sailthru): def test_drop_cookie(self, mock_sailthru, mock_get_current_request):
""" """
Test add_email_marketing_cookies Test add_email_marketing_cookies
""" """
...@@ -62,13 +81,16 @@ class EmailMarketingTests(TestCase): ...@@ -62,13 +81,16 @@ class EmailMarketingTests(TestCase):
"success": True, "success": True,
"redirect_url": 'test.com/test', "redirect_url": 'test.com/test',
}) })
self.request.COOKIES['sailthru_content'] = 'cookie_content'
mock_get_current_request.return_value = self.request
mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': {'cookie': 'test_cookie'}})) mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': {'cookie': 'test_cookie'}}))
add_email_marketing_cookies(None, response=response, user=self.user) add_email_marketing_cookies(None, response=response, user=self.user)
mock_sailthru.assert_called_with('user',
{'fields': {'keys': 1},
'cookies': {'sailthru_content': 'cookie_content'},
'id': TEST_EMAIL,
'vars': {'last_login_date': ANY}})
self.assertTrue('sailthru_hid' in response.cookies) self.assertTrue('sailthru_hid' in response.cookies)
self.assertEquals(mock_sailthru.call_args[0][0], "user")
userparms = mock_sailthru.call_args[0][1]
self.assertEquals(userparms['fields']['keys'], 1)
self.assertEquals(userparms['id'], TEST_EMAIL)
self.assertEquals(response.cookies['sailthru_hid'].value, "test_cookie") self.assertEquals(response.cookies['sailthru_hid'].value, "test_cookie")
@patch('email_marketing.signals.SailthruClient.api_post') @patch('email_marketing.signals.SailthruClient.api_post')
...@@ -111,7 +133,7 @@ class EmailMarketingTests(TestCase): ...@@ -111,7 +133,7 @@ class EmailMarketingTests(TestCase):
self.assertEquals(userparms['lists']['new list'], 1) self.assertEquals(userparms['lists']['new list'], 1)
@patch('email_marketing.tasks.SailthruClient.api_post') @patch('email_marketing.tasks.SailthruClient.api_post')
def test_activation(self, mock_sailthru): def test_user_activation(self, mock_sailthru):
""" """
test send of activation template test send of activation template
""" """
...@@ -125,7 +147,7 @@ class EmailMarketingTests(TestCase): ...@@ -125,7 +147,7 @@ class EmailMarketingTests(TestCase):
@patch('email_marketing.tasks.log.error') @patch('email_marketing.tasks.log.error')
@patch('email_marketing.tasks.SailthruClient.api_post') @patch('email_marketing.tasks.SailthruClient.api_post')
def test_error_logging(self, mock_sailthru, mock_log_error): def test_update_user_error_logging(self, mock_sailthru, mock_log_error):
""" """
Ensure that error returned from Sailthru api is logged Ensure that error returned from Sailthru api is logged
""" """
...@@ -133,28 +155,121 @@ class EmailMarketingTests(TestCase): ...@@ -133,28 +155,121 @@ class EmailMarketingTests(TestCase):
update_user.delay(self.user.username) update_user.delay(self.user.username)
self.assertTrue(mock_log_error.called) self.assertTrue(mock_log_error.called)
# force Sailthru API exception
mock_sailthru.side_effect = SailthruClientError
update_user.delay(self.user.username)
self.assertTrue(mock_log_error.called)
# force Sailthru API exception on 2nd call
mock_sailthru.side_effect = [None, SailthruClientError]
mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True}))
update_user.delay(self.user.username, new_user=True)
self.assertTrue(mock_log_error.called)
# force Sailthru API error return on 2nd call
mock_sailthru.side_effect = None
mock_sailthru.return_value = [SailthruResponse(JsonResponse({'ok': True})),
SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))]
update_user.delay(self.user.username, new_user=True)
self.assertTrue(mock_log_error.called)
@patch('email_marketing.tasks.log.error') @patch('email_marketing.tasks.log.error')
@patch('email_marketing.tasks.SailthruClient.api_post') @patch('email_marketing.tasks.SailthruClient.api_post')
def test_just_return(self, mock_sailthru, mock_log_error): def test_update_user_error_logging_bad_user(self, mock_sailthru, mock_log_error):
"""
Test update_user with invalid user
"""
update_user.delay('baduser')
self.assertTrue(mock_log_error.called)
self.assertFalse(mock_sailthru.called)
update_user_email.delay('baduser', 'aa@bb.com')
self.assertTrue(mock_log_error.called)
self.assertFalse(mock_sailthru.called)
@patch('email_marketing.tasks.log.error')
@patch('email_marketing.tasks.SailthruClient.api_post')
def test_just_return_tasks(self, mock_sailthru, mock_log_error):
""" """
Ensure that disabling Sailthru just returns Ensure that disabling Sailthru just returns
""" """
update_email_marketing_config(enabled=False) update_email_marketing_config(enabled=False)
update_user.delay(self.user.username) update_user.delay(self.user.username)
self.assertFalse(mock_log_error.called) self.assertFalse(mock_log_error.called)
self.assertFalse(mock_sailthru.called) self.assertFalse(mock_sailthru.called)
update_user_email.delay(self.user.username, "newemail2@test.com") update_user_email.delay(self.user.username, "newemail2@test.com")
self.assertFalse(mock_log_error.called) self.assertFalse(mock_log_error.called)
self.assertFalse(mock_sailthru.called) self.assertFalse(mock_sailthru.called)
update_course_enrollment.delay(self.user.username, TEST_EMAIL, 'http://course',
EnrollStatusChange.enroll, 'audit')
self.assertFalse(mock_log_error.called)
self.assertFalse(mock_sailthru.called)
update_email_marketing_config(enabled=True)
@patch('email_marketing.signals.log.error')
def test_just_return_signals(self, mock_log_error):
"""
Ensure that disabling Sailthru just returns
"""
update_email_marketing_config(enabled=False)
handle_enroll_status_change(None)
self.assertFalse(mock_log_error.called)
add_email_marketing_cookies(None)
self.assertFalse(mock_log_error.called)
email_marketing_register_user(None)
self.assertFalse(mock_log_error.called)
update_email_marketing_config(enabled=True) update_email_marketing_config(enabled=True)
# test anonymous users
anon = AnonymousUser()
email_marketing_register_user(None, user=anon)
self.assertFalse(mock_log_error.called)
email_marketing_user_field_changed(None, user=anon)
self.assertFalse(mock_log_error.called)
@patch('email_marketing.signals.crum.get_current_request')
@patch('lms.djangoapps.email_marketing.tasks.update_course_enrollment.delay')
def test_handle_enroll_status_change(self, mock_update_course_enrollment, mock_get_current_request):
"""
Test that the enroll status change signal handler properly calls the task routine
"""
# should just return if no current request found
mock_get_current_request.return_value = None
handle_enroll_status_change(None)
self.assertFalse(mock_update_course_enrollment.called)
# now test with current request
mock_get_current_request.return_value = self.request
self.request.COOKIES['sailthru_bid'] = 'cookie_bid'
handle_enroll_status_change(None, event=EnrollStatusChange.enroll,
user=self.user,
mode='audit', course_id=self.course_id,
cost=None, currency=None)
self.assertTrue(mock_update_course_enrollment.called)
mock_update_course_enrollment.assert_called_with(TEST_EMAIL,
self.course_url,
EnrollStatusChange.enroll,
'audit',
course_id=self.course_id,
currency=None,
message_id='cookie_bid',
unit_cost=None)
@patch('email_marketing.tasks.SailthruClient.api_post') @patch('email_marketing.tasks.SailthruClient.api_post')
def test_change_email(self, mock_sailthru): def test_change_email(self, mock_sailthru):
""" """
test async method in task that changes email in Sailthru test async method in task that changes email in Sailthru
""" """
mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True})) mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True}))
#self.user.email = "newemail@test.com"
update_user_email.delay(self.user.username, "old@edx.org") update_user_email.delay(self.user.username, "old@edx.org")
self.assertEquals(mock_sailthru.call_args[0][0], "user") self.assertEquals(mock_sailthru.call_args[0][0], "user")
userparms = mock_sailthru.call_args[0][1] userparms = mock_sailthru.call_args[0][1]
...@@ -163,6 +278,229 @@ class EmailMarketingTests(TestCase): ...@@ -163,6 +278,229 @@ class EmailMarketingTests(TestCase):
self.assertEquals(userparms['keys']['email'], TEST_EMAIL) self.assertEquals(userparms['keys']['email'], TEST_EMAIL)
@patch('email_marketing.tasks.log.error') @patch('email_marketing.tasks.log.error')
@patch('email_marketing.tasks.SailthruClient.purchase')
@patch('email_marketing.tasks.SailthruClient.api_get')
@patch('email_marketing.tasks.SailthruClient.api_post')
@patch('email_marketing.tasks.get_course_by_id')
def test_update_course_enrollment(self, mock_get_course, mock_sailthru_api_post,
mock_sailthru_api_get, mock_sailthru_purchase, mock_log_error):
"""
test async method in task posts enrolls and purchases
"""
CourseMode.objects.create(
course_id=self.course_id,
mode_slug=CourseMode.AUDIT,
mode_display_name=CourseMode.AUDIT
)
CourseMode.objects.create(
course_id=self.course_id,
mode_slug=CourseMode.VERIFIED,
mode_display_name=CourseMode.VERIFIED,
min_price=49,
expiration_datetime=datetime.date(2020, 3, 12)
)
mock_get_course.return_value = {'display_name': "Test Title"}
mock_sailthru_api_post.return_value = SailthruResponse(JsonResponse({'ok': True}))
mock_sailthru_api_get.return_value = SailthruResponse(JsonResponse({'vars': {'unenrolled': ['course_u1']}}))
mock_sailthru_purchase.return_value = SailthruResponse(JsonResponse({'ok': True}))
# test enroll
mock_get_course.side_effect = Http404
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.enroll,
'audit',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=0)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'audit',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: audit',
'url': self.course_url,
'price': 100, 'qty': 1, 'id': self.course_id_string + '-audit'}],
options={'send_template': 'enroll_template'},
incomplete=None, message_id='cookie_bid')
# test unenroll
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.unenroll,
'audit',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=0)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'audit',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: audit',
'url': self.course_url,
'price': 100, 'qty': 1, 'id': self.course_id_string + '-audit'}],
options={'send_template': 'enroll_template'},
incomplete=None, message_id='cookie_bid')
# test add upgrade to cart
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.upgrade_start,
'verified',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=49)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'verified',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: verified',
'url': self.course_url,
'price': 4900, 'qty': 1, 'id': self.course_id_string + '-verified'}],
options={'reminder_template': 'abandoned_template', 'reminder_time': '+60 minutes'},
incomplete=1, message_id='cookie_bid')
# test add purchase to cart
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.paid_start,
'honor',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=49)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'honor',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: honor',
'url': self.course_url,
'price': 4900, 'qty': 1, 'id': self.course_id_string + '-honor'}],
options={'reminder_template': 'abandoned_template', 'reminder_time': '+60 minutes'},
incomplete=1, message_id='cookie_bid')
# test purchase complete
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.paid_complete,
'honor',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=99)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'honor',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: honor',
'url': self.course_url,
'price': 9900, 'qty': 1, 'id': self.course_id_string + '-honor'}],
options={'send_template': 'purchase_template'},
incomplete=None, message_id='cookie_bid')
# test upgrade complete
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.upgrade_complete,
'verified',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=99)
mock_sailthru_purchase.assert_called_with(TEST_EMAIL, [{'vars': {'course_run_id': self.course_id_string, 'mode': 'verified',
'upgrade_deadline_verified': '2020-03-12'},
'title': 'Course ' + self.course_id_string + ' mode: verified',
'url': self.course_url,
'price': 9900, 'qty': 1, 'id': self.course_id_string + '-verified'}],
options={'send_template': 'upgrade_template'},
incomplete=None, message_id='cookie_bid')
# test purchase API error
mock_sailthru_purchase.return_value = SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.upgrade_complete,
'verified',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=99)
self.assertTrue(mock_log_error.called)
# test purchase API exception
mock_sailthru_purchase.side_effect = SailthruClientError
update_course_enrollment.delay(TEST_EMAIL,
self.course_url,
EnrollStatusChange.upgrade_complete,
'verified',
course_id=self.course_id,
currency='USD',
message_id='cookie_bid',
unit_cost=99)
self.assertTrue(mock_log_error.called)
@patch('email_marketing.tasks.SailthruClient')
def test_get_course_content(self, mock_sailthru_client):
"""
test routine which fetches data from Sailthru content api
"""
mock_sailthru_client.api_get.return_value = SailthruResponse(JsonResponse({"title": "The title"}))
response_json = _get_course_content('course:123', mock_sailthru_client, EmailMarketingConfiguration.current())
self.assertEquals(response_json, {"title": "The title"})
mock_sailthru_client.api_get.assert_called_with('content', {'id': 'course:123'})
# test second call uses cache
response_json = _get_course_content('course:123', mock_sailthru_client, EmailMarketingConfiguration.current())
self.assertEquals(response_json, {"title": "The title"})
mock_sailthru_client.api_get.assert_not_called()
# test error from Sailthru
mock_sailthru_client.api_get.return_value = \
SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))
self.assertEquals(_get_course_content('course:124', mock_sailthru_client, EmailMarketingConfiguration.current()), {})
# test exception
mock_sailthru_client.api_get.side_effect = SailthruClientError
self.assertEquals(_get_course_content('course:125', mock_sailthru_client, EmailMarketingConfiguration.current()), {})
@patch('email_marketing.tasks.SailthruClient')
def test_update_unenrolled_list(self, mock_sailthru_client):
"""
test routine which updates the unenrolled list in Sailthru
"""
# test a new unenroll
mock_sailthru_client.api_get.return_value = \
SailthruResponse(JsonResponse({'vars': {'unenrolled': ['course_u1']}}))
self.assertTrue(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, True))
mock_sailthru_client.api_get.assert_called_with("user", {"id": TEST_EMAIL, "fields": {"vars": 1}})
mock_sailthru_client.api_post.assert_called_with('user',
{'vars': {'unenrolled': ['course_u1', self.course_url]},
'id': TEST_EMAIL, 'key': 'email'})
# test an enroll of a previously unenrolled course
mock_sailthru_client.api_get.return_value = \
SailthruResponse(JsonResponse({'vars': {'unenrolled': [self.course_url]}}))
self.assertTrue(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, False))
mock_sailthru_client.api_post.assert_called_with('user',
{'vars': {'unenrolled': []},
'id': TEST_EMAIL, 'key': 'email'})
# test get error from Sailthru
mock_sailthru_client.api_get.return_value = \
SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))
self.assertFalse(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, False))
# test post error from Sailthru
mock_sailthru_client.api_post.return_value = \
SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))
mock_sailthru_client.api_get.return_value = \
SailthruResponse(JsonResponse({'vars': {'unenrolled': [self.course_url]}}))
self.assertFalse(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, False))
# test exception
mock_sailthru_client.api_get.side_effect = SailthruClientError
self.assertFalse(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, False))
@patch('email_marketing.tasks.log.error')
@patch('email_marketing.tasks.SailthruClient.api_post') @patch('email_marketing.tasks.SailthruClient.api_post')
def test_error_logging1(self, mock_sailthru, mock_log_error): def test_error_logging1(self, mock_sailthru, mock_log_error):
""" """
...@@ -172,6 +510,10 @@ class EmailMarketingTests(TestCase): ...@@ -172,6 +510,10 @@ class EmailMarketingTests(TestCase):
update_user_email.delay(self.user.username, "newemail2@test.com") update_user_email.delay(self.user.username, "newemail2@test.com")
self.assertTrue(mock_log_error.called) self.assertTrue(mock_log_error.called)
mock_sailthru.side_effect = SailthruClientError
update_user_email.delay(self.user.username, "newemail2@test.com")
self.assertTrue(mock_log_error.called)
@patch('lms.djangoapps.email_marketing.tasks.update_user.delay') @patch('lms.djangoapps.email_marketing.tasks.update_user.delay')
def test_register_user(self, mock_update_user): def test_register_user(self, mock_update_user):
""" """
......
...@@ -38,7 +38,7 @@ from courseware.courses import get_course_by_id ...@@ -38,7 +38,7 @@ from courseware.courses import get_course_by_id
from config_models.models import ConfigurationModel from config_models.models import ConfigurationModel
from course_modes.models import CourseMode from course_modes.models import CourseMode
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from student.models import CourseEnrollment, UNENROLL_DONE from student.models import CourseEnrollment, UNENROLL_DONE, EnrollStatusChange
from util.query import use_read_replica_if_available from util.query import use_read_replica_if_available
from xmodule_django.models import CourseKeyField from xmodule_django.models import CourseKeyField
from .exceptions import ( from .exceptions import (
...@@ -1587,6 +1587,10 @@ class PaidCourseRegistration(OrderItem): ...@@ -1587,6 +1587,10 @@ class PaidCourseRegistration(OrderItem):
item.save() item.save()
log.info("User {} added course registration {} to cart: order {}" log.info("User {} added course registration {} to cart: order {}"
.format(order.user.email, course_id, order.id)) .format(order.user.email, course_id, order.id))
CourseEnrollment.send_signal_full(EnrollStatusChange.paid_start,
user=order.user, mode=item.mode, course_id=course_id,
cost=cost, currency=currency)
return item return item
def purchased_callback(self): def purchased_callback(self):
...@@ -1607,6 +1611,8 @@ class PaidCourseRegistration(OrderItem): ...@@ -1607,6 +1611,8 @@ class PaidCourseRegistration(OrderItem):
log.info("Enrolled {0} in paid course {1}, paid ${2}" log.info("Enrolled {0} in paid course {1}, paid ${2}"
.format(self.user.email, self.course_id, self.line_cost)) .format(self.user.email, self.course_id, self.line_cost))
self.course_enrollment.send_signal(EnrollStatusChange.paid_complete,
cost=self.line_cost, currency=self.currency)
def generate_receipt_instructions(self): def generate_receipt_instructions(self):
""" """
...@@ -1977,6 +1983,9 @@ class CertificateItem(OrderItem): ...@@ -1977,6 +1983,9 @@ class CertificateItem(OrderItem):
order.currency = currency order.currency = currency
order.save() order.save()
item.save() item.save()
# signal course added to cart
course_enrollment.send_signal(EnrollStatusChange.paid_start, cost=cost, currency=currency)
return item return item
def purchased_callback(self): def purchased_callback(self):
...@@ -1985,6 +1994,8 @@ class CertificateItem(OrderItem): ...@@ -1985,6 +1994,8 @@ class CertificateItem(OrderItem):
""" """
self.course_enrollment.change_mode(self.mode) self.course_enrollment.change_mode(self.mode)
self.course_enrollment.activate() self.course_enrollment.activate()
self.course_enrollment.send_signal(EnrollStatusChange.upgrade_complete,
cost=self.unit_cost, currency=self.currency)
def additional_instruction_text(self): def additional_instruction_text(self):
verification_reminder = "" verification_reminder = ""
......
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