Commit 650e2508 by PaulWattenberger Committed by GitHub

Merge pull request #872 from edx/pwattenberger/sailthru2

Sailthru support for recording purchases/enrolls
parents 4fd1bd64 09ebd176
default_app_config = 'ecommerce.sailthru.apps.SailthruAppConfig' # pragma: no cover
from django.apps import AppConfig
class SailthruAppConfig(AppConfig):
name = 'ecommerce.sailthru'
verbose_name = 'Sailthru'
def ready(self):
super(SailthruAppConfig, self).ready()
# noinspection PyUnresolvedReferences
import ecommerce.sailthru.signals # pylint: disable=unused-variable
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def create_switch(apps, schema_editor):
"""Create and activate the sailthru_enable switch if it does not already exist."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.get_or_create(name='sailthru_enable', defaults={'active': False})
def delete_switch(apps, schema_editor):
"""Delete the sailthru_enable switch."""
Switch = apps.get_model('waffle', 'Switch')
Switch.objects.filter(name='sailthru_enable').delete()
class Migration(migrations.Migration):
dependencies = [
('waffle', '0001_initial'),
]
operations = [
migrations.RunPython(create_switch, reverse_code=delete_switch),
]
import logging
from django.dispatch import receiver
from oscar.core.loading import get_class
import waffle
from ecommerce_worker.sailthru.v1.tasks import update_course_enrollment
from ecommerce.extensions.analytics.utils import silence_exceptions
from ecommerce.core.url_utils import get_lms_url
from ecommerce.courses.utils import mode_for_seat
logger = logging.getLogger(__name__)
post_checkout = get_class('checkout.signals', 'post_checkout')
basket_addition = get_class('basket.signals', 'basket_addition')
@receiver(post_checkout)
@silence_exceptions("Failed to call Sailthru upon order completion.")
def process_checkout_complete(sender, order=None, request=None, user=None, **kwargs): # pylint: disable=unused-argument
"""Tell Sailthru when payment done.
Arguments:
Parameters described at http://django-oscar.readthedocs.io/en/releases-1.1/ref/signals.html
"""
if not waffle.switch_is_active('sailthru_enable'):
return
# loop through lines in order
# If multi product orders become common it may be worthwhile to pass an array of
# orders to the worker in one call to save overhead, however, that would be difficult
# because of the fact that there are different templates for free enroll versus paid enroll
for line in order.lines.all():
# get product
product = line.product
# get price
price = line.line_price_excl_tax
course_id = product.course_id
# figure out course url
course_url = _build_course_url(course_id)
# pass event to ecommerce_worker.sailthru.v1.tasks to handle asynchronously
update_course_enrollment.delay(user.email, course_url, False, mode_for_seat(product),
unit_cost=price, course_id=course_id, currency=order.currency,
site_code=request.site.siteconfiguration.partner.short_code,
message_id=request.COOKIES.get('sailthru_bid'))
@receiver(basket_addition)
@silence_exceptions("Failed to call Sailthru upon basket addition.")
def process_basket_addition(sender, product=None, request=None, user=None, **kwargs): # pylint: disable=unused-argument
"""Tell Sailthru when payment started.
Arguments:
Parameters described at http://django-oscar.readthedocs.io/en/releases-1.1/ref/signals.html
"""
if not waffle.switch_is_active('sailthru_enable'):
return
course_id = product.course_id
# figure out course url
course_url = _build_course_url(course_id)
# get price & currency
stock_record = product.stockrecords.first()
if stock_record:
price = stock_record.price_excl_tax
currency = stock_record.price_currency
# return if no price, no need to add free items to shopping cart
if not price:
return
# pass event to ecommerce_worker.sailthru.v1.tasks to handle asynchronously
update_course_enrollment.delay(user.email, course_url, True, mode_for_seat(product),
unit_cost=price, course_id=course_id, currency=currency,
site_code=request.site.siteconfiguration.partner.short_code,
message_id=request.COOKIES.get('sailthru_bid'))
def _build_course_url(course_id):
"""Build a course url from a course id and the host"""
return get_lms_url('courses/{}/info'.format(course_id))
"""Tests of ecommerce sailthru signal handlers."""
import logging
from mock import patch
from oscar.test.factories import create_order
from oscar.test.newfactories import UserFactory, BasketFactory
from django.test.client import RequestFactory
from ecommerce.tests.testcases import TestCase
from ecommerce.core.tests import toggle_switch
from ecommerce.sailthru.signals import process_checkout_complete, process_basket_addition
from ecommerce.courses.models import Course
from ecommerce.extensions.catalogue.tests.mixins import CourseCatalogTestMixin
log = logging.getLogger(__name__)
TEST_EMAIL = "test@edx.org"
class SailthruTests(CourseCatalogTestMixin, TestCase):
"""
Tests for the Sailthru signals class.
"""
def setUp(self):
super(SailthruTests, self).setUp()
self.request_factory = RequestFactory()
self.request = self.request_factory.get("foo")
self.request.COOKIES['sailthru_bid'] = 'cookie_bid'
self.request.site = self.site
self.user = UserFactory.create(username='test', email=TEST_EMAIL)
toggle_switch('sailthru_enable', True)
# create some test course objects
self.course_id = 'edX/toy/2012_Fall'
self.course_url = 'http://lms.testserver.fake/courses/edX/toy/2012_Fall/info'
self.course = Course.objects.create(id=self.course_id, name='Demo Course')
@patch('ecommerce.sailthru.signals.logger.error')
def test_just_return_signals(self, mock_log_error):
"""
Ensure that disabling Sailthru just returns
"""
toggle_switch('sailthru_enable', False)
process_checkout_complete(None)
self.assertFalse(mock_log_error.called)
process_basket_addition(None)
self.assertFalse(mock_log_error.called)
@patch('ecommerce_worker.sailthru.v1.tasks.update_course_enrollment.delay')
def test_process_checkout_complete(self, mock_update_course_enrollment):
"""
Test that the process_checkout signal handler properly calls the task routine
"""
seat, order = self._create_order(99)
process_checkout_complete(None, request=self.request,
user=self.user,
order=order)
self.assertTrue(mock_update_course_enrollment.called)
mock_update_course_enrollment.assert_called_with(TEST_EMAIL,
self.course_url,
False,
seat.attr.certificate_type,
course_id=self.course_id,
currency=order.currency,
message_id='cookie_bid',
site_code='edX',
unit_cost=order.total_excl_tax)
@patch('ecommerce_worker.sailthru.v1.tasks.update_course_enrollment.delay')
def test_process_basket_addition(self, mock_update_course_enrollment):
"""
Test that the process_basket_addition signal handler properly calls the task routine
"""
seat, order = self._create_order(99)
process_basket_addition(None, request=self.request,
user=self.user,
product=seat)
self.assertTrue(mock_update_course_enrollment.called)
mock_update_course_enrollment.assert_called_with(TEST_EMAIL,
self.course_url,
True,
seat.attr.certificate_type,
course_id=self.course_id,
currency=order.currency,
message_id='cookie_bid',
site_code='edX',
unit_cost=order.total_excl_tax)
@patch('ecommerce_worker.sailthru.v1.tasks.update_course_enrollment.delay')
def test_price_zero(self, mock_update_course_enrollment):
"""
Test that a price of zero skips update_course_enrollment in process basket
"""
seat = self._create_order(0)[0]
process_basket_addition(None, request=self.request,
user=self.user,
product=seat)
self.assertFalse(mock_update_course_enrollment.called)
def _create_order(self, price):
seat = self.course.create_or_update_seat('verified', False, price, self.partner, None)
basket = BasketFactory()
basket.add_product(seat, 1)
order = create_order(number=1, basket=basket, user=self.user)
order.total_excl_tax = price
return seat, order
......@@ -270,6 +270,9 @@ LOCAL_APPS = [
# Theming app for customizing visual and behavioral attributes of a site
'ecommerce.theming',
# Sailthru email marketing integration
'ecommerce.sailthru',
]
# See: https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps
......@@ -483,7 +486,8 @@ CELERY_IMPORTS = (
'ecommerce_worker.fulfillment.v1.tasks',
)
CELERY_ROUTES = {'ecommerce_worker.fulfillment.v1.tasks.fulfill_order': {'queue': 'fulfillment'}}
CELERY_ROUTES = {'ecommerce_worker.fulfillment.v1.tasks.fulfill_order': {'queue': 'fulfillment'},
'ecommerce_worker.sailthru.v1.tasks.update_course_enrollment': {'queue': 'email_marketing'}}
# Prevent Celery from removing handlers on the root logger. Allows setting custom logging handlers.
# See http://celery.readthedocs.org/en/latest/configuration.html#celeryd-hijack-root-logger.
......
......@@ -17,7 +17,7 @@ edx-auth-backends==0.5.0
edx-django-release-util==0.0.3
edx-django-sites-extensions==1.0.0
edx-drf-extensions==1.0.0
edx-ecommerce-worker==0.4.1
edx-ecommerce-worker==0.5.0
edx-opaque-keys==0.3.1
edx-rest-api-client==1.6.0
jsonfield==1.0.3
......@@ -30,4 +30,5 @@ pycountry==1.18
python-dateutil==2.4.2
pytz==2015.7
requests==2.9.1
sailthru-client==2.2.3
suds==0.4
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