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
......
...@@ -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