From ca02235e6470bd77923610e2cc7aecb5b1e14157 Mon Sep 17 00:00:00 2001 From: PaulWattenberger Date: Wed, 8 Jun 2016 19:38:29 -0400 Subject: [PATCH] Pwattenberger/sailthru (#12646) * Initial commit of Sailthru lms changes * Field mapping changes for Sailthru integration * Merge fix * Add users to Sailthru list on registration * FIx minor code format issues * Several updates based on code review feedback * Updates based on recomendations from Sailthru * Clean up unit tests * Fix last login * Updates based on code review feedback * Updates based on code review feedback * Fix comment * Cleanup --- common/djangoapps/student/cookies.py | 7 + common/djangoapps/student/views.py | 7 +- common/djangoapps/util/model_utils.py | 7 + lms/djangoapps/email_marketing/__init__.py | 0 lms/djangoapps/email_marketing/admin.py | 8 + .../migrations/0001_initial.py | 50 +++++ .../email_marketing/migrations/__init__.py | 0 lms/djangoapps/email_marketing/models.py | 57 ++++++ lms/djangoapps/email_marketing/signals.py | 144 ++++++++++++++ lms/djangoapps/email_marketing/startup.py | 3 + lms/djangoapps/email_marketing/tasks.py | 158 ++++++++++++++++ .../email_marketing/tests/test_signals.py | 178 ++++++++++++++++++ lms/envs/common.py | 3 + requirements/edx/base.txt | 3 + 14 files changed, 624 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/email_marketing/__init__.py create mode 100644 lms/djangoapps/email_marketing/admin.py create mode 100644 lms/djangoapps/email_marketing/migrations/0001_initial.py create mode 100644 lms/djangoapps/email_marketing/migrations/__init__.py create mode 100644 lms/djangoapps/email_marketing/models.py create mode 100644 lms/djangoapps/email_marketing/signals.py create mode 100644 lms/djangoapps/email_marketing/startup.py create mode 100644 lms/djangoapps/email_marketing/tasks.py create mode 100644 lms/djangoapps/email_marketing/tests/test_signals.py diff --git a/common/djangoapps/student/cookies.py b/common/djangoapps/student/cookies.py index 694e06f720..b508f93339 100644 --- a/common/djangoapps/student/cookies.py +++ b/common/djangoapps/student/cookies.py @@ -5,10 +5,14 @@ Utility functions for setting "logged in" cookies used by subdomains. import time import json +from django.dispatch import Signal + from django.utils.http import cookie_date from django.conf import settings from django.core.urlresolvers import reverse, NoReverseMatch +CREATE_LOGON_COOKIE = Signal(providing_args=["user", "response"]) + def set_logged_in_cookies(request, response, user): """ @@ -118,6 +122,9 @@ def set_logged_in_cookies(request, response, user): **cookie_settings ) + # give signal receivers a chance to add cookies + CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response) + return response diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 13624e6376..c71c829439 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -34,7 +34,7 @@ from django.utils.translation import ugettext as _, get_language from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie from django.views.decorators.http import require_POST, require_GET from django.db.models.signals import post_save -from django.dispatch import receiver +from django.dispatch import receiver, Signal from django.template.response import TemplateResponse from ratelimitbackend.exceptions import RateLimitException @@ -135,6 +135,8 @@ 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' +# used to announce a registration +REGISTER_USER = Signal(providing_args=["user", "profile"]) # Disable this warning because it doesn't make sense to completely refactor tests to appease Pylint # pylint: disable=logging-format-interpolation @@ -1754,6 +1756,9 @@ def create_account_with_params(request, params): } ) + # Announce registration + REGISTER_USER.send(sender=None, user=user, profile=profile) + create_comments_service_user(user) # Don't send email if we are: diff --git a/common/djangoapps/util/model_utils.py b/common/djangoapps/util/model_utils.py index de8e43a7d8..06cbfac85e 100644 --- a/common/djangoapps/util/model_utils.py +++ b/common/djangoapps/util/model_utils.py @@ -9,11 +9,14 @@ from eventtracking import tracker from django.conf import settings from django.utils.encoding import force_unicode from django.utils.safestring import mark_safe +from django.dispatch import Signal from django_countries.fields import Country # The setting name used for events when "settings" (account settings, preferences, profile information) change. USER_SETTINGS_CHANGED_EVENT_NAME = u'edx.user.settings.changed' +# Used to signal a field value change +USER_FIELD_CHANGED = Signal(providing_args=["user", "table", "setting", "old_value", "new_value"]) def get_changed_fields_dict(instance, model_class): @@ -152,6 +155,10 @@ def emit_setting_changed_event(user, db_table, setting_name, old_value, new_valu truncated_fields ) + # Announce field change + USER_FIELD_CHANGED.send(sender=None, user=user, table=db_table, setting=setting_name, + old_value=old_value, new_value=new_value) + def _get_truncated_setting_value(value, max_length=None): """ diff --git a/lms/djangoapps/email_marketing/__init__.py b/lms/djangoapps/email_marketing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/email_marketing/admin.py b/lms/djangoapps/email_marketing/admin.py new file mode 100644 index 0000000000..6ec36bd383 --- /dev/null +++ b/lms/djangoapps/email_marketing/admin.py @@ -0,0 +1,8 @@ +""" Admin site bindings for email marketing """ + +from django.contrib import admin + +from email_marketing.models import EmailMarketingConfiguration +from config_models.admin import ConfigurationModelAdmin + +admin.site.register(EmailMarketingConfiguration, ConfigurationModelAdmin) diff --git a/lms/djangoapps/email_marketing/migrations/0001_initial.py b/lms/djangoapps/email_marketing/migrations/0001_initial.py new file mode 100644 index 0000000000..e5a9e78cd8 --- /dev/null +++ b/lms/djangoapps/email_marketing/migrations/0001_initial.py @@ -0,0 +1,50 @@ +# -*- 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), + ] + + operations = [ + migrations.CreateModel( + name='EmailMarketingConfiguration', + 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')), + + ('sailthru_key', + models.CharField(help_text='Sailthru api key.', max_length=32, null=True, blank=True)), + + ('sailthru_secret', + models.CharField(help_text='Sailthru secret.', max_length=32, null=True, blank=True)), + + ('sailthru_new_user_list', + models.CharField(help_text='Sailthru new user list.', max_length=48, null=True, blank=True)), + + ('sailthru_retry_interval', + models.IntegerField(default=3600, help_text='Sailthru connection retry interval (secs).')), + + ('sailthru_max_retries', + models.IntegerField(default=24, help_text='Sailthru maximum retries.')), + + ('sailthru_activation_template', + models.CharField(help_text='Sailthru activation template.', max_length=20, null=True, blank=True)), + + ('changed_by', + models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')), + ], + ), + ] \ No newline at end of file diff --git a/lms/djangoapps/email_marketing/migrations/__init__.py b/lms/djangoapps/email_marketing/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/email_marketing/models.py b/lms/djangoapps/email_marketing/models.py new file mode 100644 index 0000000000..a34847436c --- /dev/null +++ b/lms/djangoapps/email_marketing/models.py @@ -0,0 +1,57 @@ +""" +Email-marketing-related models. +""" +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from config_models.models import ConfigurationModel + + +class EmailMarketingConfiguration(ConfigurationModel): + """ Email marketing configuration """ + + class Meta(object): + app_label = "email_marketing" + + sailthru_key = models.fields.CharField( + max_length=32, + help_text=_( + "API key for accessing Sailthru. " + ) + ) + + sailthru_secret = models.fields.CharField( + max_length=32, + help_text=_( + "API secret for accessing Sailthru. " + ) + ) + + sailthru_new_user_list = models.fields.CharField( + max_length=48, + help_text=_( + "Sailthru list name to add new users to. " + ) + ) + + sailthru_retry_interval = models.fields.IntegerField( + default=3600, + help_text=_( + "Sailthru connection retry interval (secs)." + ) + ) + + sailthru_max_retries = models.fields.IntegerField( + default=24, + help_text=_( + "Sailthru maximum retries." + ) + ) + + sailthru_activation_template = models.fields.CharField( + max_length=20, + blank=True, + help_text=_( + "Sailthru template to use on activation send. " + ) + ) diff --git a/lms/djangoapps/email_marketing/signals.py b/lms/djangoapps/email_marketing/signals.py new file mode 100644 index 0000000000..6ed86b8acc --- /dev/null +++ b/lms/djangoapps/email_marketing/signals.py @@ -0,0 +1,144 @@ +""" +This module contains signals needed for email integration +""" +import logging +import datetime + +from django.dispatch import receiver + +from student.models import CourseEnrollment, UNENROLL_DONE +from student.cookies import CREATE_LOGON_COOKIE +from student.views import REGISTER_USER +from email_marketing.models import EmailMarketingConfiguration +from util.model_utils import USER_FIELD_CHANGED +from lms.djangoapps.email_marketing.tasks import update_user, update_user_email + +from sailthru.sailthru_client import SailthruClient +from sailthru.sailthru_error import SailthruClientError + +log = logging.getLogger(__name__) + +# list of changed fields to pass to Sailthru +CHANGED_FIELDNAMES = ['username', 'is_active', 'name', 'gender', 'education', + 'age', 'level_of_education', 'year_of_birth', + 'country'] + + +@receiver(UNENROLL_DONE) +def handle_unenroll_done(sender, course_enrollment=None, skip_refund=False, + **kwargs): # pylint: disable=unused-argument + """ + Signal receiver for unenrollments + """ + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return + + # TBD + + +@receiver(CREATE_LOGON_COOKIE) +def add_email_marketing_cookies(sender, response=None, user=None, + **kwargs): # pylint: disable=unused-argument): + """ + Signal function for adding any cookies needed for email marketing + + Args: + response: http response object + user: The user object for the user being changed + + Returns: + response: http response object with cookie added + """ + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return response + + try: + sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) + sailthru_response = \ + sailthru_client.api_post("user", {'id': user.email, 'fields': {'keys': 1}, + 'vars': {'last_login_date': + datetime.datetime.now().strftime("%Y-%m-%d")}}) + except SailthruClientError as exc: + log.error("Exception attempting to obtain cookie from Sailthru: %s", unicode(exc)) + return response + + if sailthru_response.is_ok(): + if 'keys' in sailthru_response.json and 'cookie' in sailthru_response.json['keys']: + cookie = sailthru_response.json['keys']['cookie'] + + response.set_cookie( + 'sailthru_hid', + cookie, + max_age=365 * 24 * 60 * 60 # set for 1 year + ) + else: + log.error("No cookie returned attempting to obtain cookie from Sailthru for %s", user.email) + else: + error = sailthru_response.get_error() + log.error("Error attempting to obtain cookie from Sailthru: %s", error.get_message()) + return response + + +@receiver(REGISTER_USER) +def email_marketing_register_user(sender, user=None, profile=None, + **kwargs): # pylint: disable=unused-argument + """ + Called after user created and saved + + Args: + sender: Not used + user: The user object for the user being changed + profile: The user profile for the user being changed + kwargs: Not used + """ + log.info("Receiving REGISTER_USER") + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return + + # ignore anonymous users + if user.is_anonymous(): + return + + # perform update asynchronously + update_user.delay(user.username, new_user=True) + + +@receiver(USER_FIELD_CHANGED) +def email_marketing_user_field_changed(sender, user=None, table=None, setting=None, + old_value=None, new_value=None, **kwargs): # pylint: disable=unused-argument + """ + Update a single user/profile field + + Args: + sender: Not used + user: The user object for the user being changed + table: The name of the table being updated + setting: The name of the setting being updated + old_value: Prior value + new_value: New value + kwargs: Not used + """ + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return + + # ignore anonymous users + if user.is_anonymous(): + return + + # ignore anything but User or Profile table + if table != 'auth_user' and table != 'auth_userprofile': + return + + # ignore anything not in list of fields to handle + if setting in CHANGED_FIELDNAMES: + # perform update asynchronously, flag if activation + update_user.delay(user.username, new_user=False, + activation=(setting == 'is_active') and new_value is True) + + elif setting == 'email': + # email update is special case + update_user_email.delay(user.username, old_value) diff --git a/lms/djangoapps/email_marketing/startup.py b/lms/djangoapps/email_marketing/startup.py new file mode 100644 index 0000000000..12143730cc --- /dev/null +++ b/lms/djangoapps/email_marketing/startup.py @@ -0,0 +1,3 @@ +""" email_marketing app. """ +# this is here to support registering the signals in signals.py +from email_marketing import signals diff --git a/lms/djangoapps/email_marketing/tasks.py b/lms/djangoapps/email_marketing/tasks.py new file mode 100644 index 0000000000..7cbb8e3c8a --- /dev/null +++ b/lms/djangoapps/email_marketing/tasks.py @@ -0,0 +1,158 @@ +""" +This file contains celery tasks for email marketing signal handler. +""" +import logging +import time + +from pytz import UTC + +from celery import task +from django.contrib.auth.models import User + +from student.models import UserProfile +from email_marketing.models import EmailMarketingConfiguration + +from sailthru.sailthru_client import SailthruClient +from sailthru.sailthru_error import SailthruClientError + +log = logging.getLogger(__name__) + + +# pylint: disable=not-callable +@task(bind=True, default_retry_delay=3600, max_retries=24) +def update_user(self, username, new_user=False, activation=False): + """ + Adds/updates Sailthru profile information for a user. + Args: + username(str): A string representation of user identifier + Returns: + None + """ + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return + + # get user + user = User.objects.select_related('profile').get(username=username) + if not user: + log.error("User not found during Sailthru update %s", username) + return + + # ignore anonymous users + if user.is_anonymous(): + return + + # get 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) + try: + sailthru_response = sailthru_client.api_post("user", + _create_sailthru_user_parm(user, profile, new_user, email_config)) + except SailthruClientError as exc: + log.error("Exception attempting to add/update user %s in Sailthru - %s", username, unicode(exc)) + raise self.retry(exc=exc, + countdown=email_config.sailthru_retry_interval, + max_retries=email_config.sailthru_max_retries) + + if not sailthru_response.is_ok(): + error = sailthru_response.get_error() + # put out error and schedule retry + log.error("Error attempting to add/update user in Sailthru: %s", error.get_message()) + raise self.retry(countdown=email_config.sailthru_retry_interval, + max_retries=email_config.sailthru_max_retries) + + # if activating user, send welcome email + if activation and email_config.sailthru_activation_template: + try: + sailthru_response = sailthru_client.api_post("send", + {"email": user.email, + "template": email_config.sailthru_activation_template}) + except SailthruClientError as exc: + log.error("Exception attempting to send welcome email to user %s in Sailthru - %s", username, unicode(exc)) + raise self.retry(exc=exc, + countdown=email_config.sailthru_retry_interval, + max_retries=email_config.sailthru_max_retries) + + if not sailthru_response.is_ok(): + error = sailthru_response.get_error() + # probably an invalid template name, just put out error + log.error("Error attempting to send welcome email to user in Sailthru: %s", error.get_message()) + + +# pylint: disable=not-callable +@task(bind=True, default_retry_delay=3600, max_retries=24) +def update_user_email(self, username, old_email): + """ + Adds/updates Sailthru when a user email address is changed + Args: + username(str): A string representation of user identifier + old_email(str): Original email address + Returns: + None + """ + email_config = EmailMarketingConfiguration.current() + if not email_config.enabled: + return + + # get user + user = User.objects.get(username=username) + if not user: + log.error("User not found duing Sailthru update %s", username) + return + + # ignore anonymous users + if user.is_anonymous(): + return + + # ignore if email not changed + if user.email == old_email: + return + + sailthru_parms = {"id": old_email, "key": "email", "keysconflict": "merge", "keys": {"email": user.email}} + + try: + sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) + sailthru_response = sailthru_client.api_post("user", sailthru_parms) + except SailthruClientError as exc: + log.error("Exception attempting to update email for %s in Sailthru - %s", username, unicode(exc)) + raise self.retry(exc=exc, + countdown=email_config.sailthru_retry_interval, + max_retries=email_config.sailthru_max_retries) + + if not sailthru_response.is_ok(): + error = sailthru_response.get_error() + log.error("Error attempting to update user email address in Sailthru: %s", error.get_message()) + raise self.retry(countdown=email_config.sailthru_retry_interval, + max_retries=email_config.sailthru_max_retries) + + +def _create_sailthru_user_parm(user, profile, new_user, email_config): + """ + Create sailthru user create/update parms from user + profile. + """ + sailthru_user = {'id': user.email, 'key': 'email'} + sailthru_vars = {'username': user.username, + 'activated': int(user.is_active), + 'joined_date': user.date_joined.strftime("%Y-%m-%d")} + sailthru_user['vars'] = sailthru_vars + sailthru_vars['last_changed_time'] = int(time.time()) + + if profile: + sailthru_vars['fullname'] = profile.name + sailthru_vars['gender'] = profile.gender + sailthru_vars['education'] = profile.level_of_education + # age is not useful since it is not automatically updated + #sailthru_vars['age'] = profile.age or -1 + if profile.year_of_birth: + sailthru_vars['year_of_birth'] = profile.year_of_birth + sailthru_vars['country'] = unicode(profile.country.code) + + # if new user add to list + if new_user and email_config.sailthru_new_user_list: + sailthru_user['lists'] = {email_config.sailthru_new_user_list: 1} + + return sailthru_user diff --git a/lms/djangoapps/email_marketing/tests/test_signals.py b/lms/djangoapps/email_marketing/tests/test_signals.py new file mode 100644 index 0000000000..10dae5aa45 --- /dev/null +++ b/lms/djangoapps/email_marketing/tests/test_signals.py @@ -0,0 +1,178 @@ +"""Tests of email marketing signal handlers.""" +import logging +import ddt + +from django.test import TestCase +from django.test.utils import override_settings +from mock import patch +from util.json_request import JsonResponse + +from email_marketing.signals import handle_unenroll_done, \ + email_marketing_register_user, \ + email_marketing_user_field_changed, \ + add_email_marketing_cookies +from email_marketing.tasks import update_user, update_user_email +from email_marketing.models import EmailMarketingConfiguration +from django.test.client import RequestFactory +from student.tests.factories import UserFactory, UserProfileFactory + +from sailthru.sailthru_client import SailthruClient +from sailthru.sailthru_response import SailthruResponse +from sailthru.sailthru_error import SailthruClientError + +log = logging.getLogger(__name__) + +TEST_EMAIL = "test@edx.org" + + +def update_email_marketing_config(enabled=False, key='badkey', secret='badsecret', new_user_list='new list', + template='Activation'): + """ + Enable / Disable Sailthru integration + """ + EmailMarketingConfiguration.objects.create( + enabled=enabled, + sailthru_key=key, + sailthru_secret=secret, + sailthru_new_user_list=new_user_list, + sailthru_activation_template=template + ) + + +@ddt.ddt +class EmailMarketingTests(TestCase): + """ + Tests for the EmailMarketing signals and tasks classes. + """ + + def setUp(self): + self.request_factory = RequestFactory() + self.user = UserFactory.create(username='test', email=TEST_EMAIL) + self.profile = self.user.profile + self.request = self.request_factory.get("foo") + update_email_marketing_config(enabled=True) + super(EmailMarketingTests, self).setUp() + + @patch('email_marketing.signals.SailthruClient.api_post') + def test_drop_cookie(self, mock_sailthru): + """ + Test add_email_marketing_cookies + """ + response = JsonResponse({ + "success": True, + "redirect_url": 'test.com/test', + }) + mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': {'cookie': 'test_cookie'}})) + add_email_marketing_cookies(None, response=response, user=self.user) + 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") + + @patch('email_marketing.signals.SailthruClient.api_post') + def test_drop_cookie_error_path(self, mock_sailthru): + """ + test that error paths return no cookie + """ + response = JsonResponse({ + "success": True, + "redirect_url": 'test.com/test', + }) + mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': {'cookiexx': 'test_cookie'}})) + add_email_marketing_cookies(None, response=response, user=self.user) + self.assertFalse('sailthru_hid' in response.cookies) + + mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': "error", "errormsg": "errormsg"})) + add_email_marketing_cookies(None, response=response, user=self.user) + self.assertFalse('sailthru_hid' in response.cookies) + + mock_sailthru.side_effect = SailthruClientError + add_email_marketing_cookies(None, response=response, user=self.user) + self.assertFalse('sailthru_hid' in response.cookies) + + @patch('email_marketing.tasks.log.error') + @patch('email_marketing.tasks.SailthruClient.api_post') + def test_add_user(self, mock_sailthru, mock_log_error): + """ + test async method in tasks that actually updates Sailthru + """ + mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True})) + update_user.delay(self.user.username, new_user=True) + self.assertFalse(mock_log_error.called) + self.assertEquals(mock_sailthru.call_args[0][0], "user") + userparms = mock_sailthru.call_args[0][1] + self.assertEquals(userparms['key'], "email") + self.assertEquals(userparms['id'], TEST_EMAIL) + self.assertEquals(userparms['vars']['gender'], "m") + self.assertEquals(userparms['vars']['username'], "test") + self.assertEquals(userparms['vars']['activated'], 1) + self.assertEquals(userparms['lists']['new list'], 1) + + @patch('email_marketing.tasks.SailthruClient.api_post') + def test_activation(self, mock_sailthru): + """ + test send of activation template + """ + mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True})) + update_user.delay(self.user.username, new_user=True, activation=True) + # look for call args for 2nd call + self.assertEquals(mock_sailthru.call_args[0][0], "send") + userparms = mock_sailthru.call_args[0][1] + self.assertEquals(userparms['email'], TEST_EMAIL) + self.assertEquals(userparms['template'], "Activation") + + @patch('email_marketing.tasks.log.error') + @patch('email_marketing.tasks.SailthruClient.api_post') + def test_error_logging(self, mock_sailthru, mock_log_error): + """ + Ensure that error returned from Sailthru api is logged + """ + mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'})) + update_user.delay(self.user.username) + self.assertTrue(mock_log_error.called) + + @patch('email_marketing.tasks.SailthruClient.api_post') + def test_change_email(self, mock_sailthru): + """ + test async method in task that changes email in Sailthru + """ + mock_sailthru.return_value = SailthruResponse(JsonResponse({'ok': True})) + #self.user.email = "newemail@test.com" + update_user_email.delay(self.user.username, "old@edx.org") + self.assertEquals(mock_sailthru.call_args[0][0], "user") + userparms = mock_sailthru.call_args[0][1] + self.assertEquals(userparms['key'], "email") + self.assertEquals(userparms['id'], "old@edx.org") + self.assertEquals(userparms['keys']['email'], TEST_EMAIL) + + @patch('email_marketing.tasks.log.error') + @patch('email_marketing.tasks.SailthruClient.api_post') + def test_error_logging1(self, mock_sailthru, mock_log_error): + """ + Ensure that error returned from Sailthru api is logged + """ + mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'})) + 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') + def test_register_user(self, mock_update_user): + """ + make sure register user call invokes update_user + """ + email_marketing_register_user(None, user=self.user, profile=self.profile) + self.assertTrue(mock_update_user.called) + + @patch('lms.djangoapps.email_marketing.tasks.update_user.delay') + @ddt.data(('auth_userprofile', 'gender', 'f', True), + ('auth_user', 'is_active', 1, True), + ('auth_userprofile', 'shoe_size', 1, False)) + @ddt.unpack + def test_modify_field(self, table, setting, value, result, mock_update_user): + """ + Test that correct fields call update_user + """ + email_marketing_user_field_changed(None, self.user, table=table, setting=setting, new_value=value) + self.assertEqual(mock_update_user.called, result) diff --git a/lms/envs/common.py b/lms/envs/common.py index 3f7c85e75c..a629559e63 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2070,6 +2070,9 @@ INSTALLED_APPS = ( # Enables default site and redirects 'django_sites_extensions', + + # Email marketing integration + 'email_marketing', ) # Migrations which are not in the standard module "migrations" diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 313557b6c2..99a5f7eb52 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -180,3 +180,6 @@ jsonfield==1.0.3 # Inlines CSS styles into HTML for email notifications. pynliner==0.5.2 + +# for sailthru integration +sailthru-client==2.2.3