From 35f78a3241c69aea2adf60c0cebdc029b6c0f3eb Mon Sep 17 00:00:00 2001 From: Michael Terry Date: Mon, 3 May 2021 15:45:26 -0400 Subject: [PATCH] feat!: remove all email_marketing djangoapp code This djangoapp was designed for talking to sailthru, in a fairly edx.org-specific way. Nowadays, edx.org doesn't need this code and if other installations do, it's better off as a more distinct plugin anyway, rather than direct support in the platform. I've moved the one signal that was still useful (calling segment.identify() whenever user fields change) into user_authn. And I've left the EmailMarketingConfiguration model alone for now, but will remove that shortly. Nothing uses it as of this commit. AA-607 DEPR-139 --- common/djangoapps/student/models.py | 2 - common/djangoapps/student/signals/__init__.py | 1 - common/djangoapps/student/signals/signals.py | 1 - common/djangoapps/util/model_utils.py | 5 - .../0007-sys-path-modification-removal.rst | 2 - docs/guides/docstrings/lms_index.rst | 1 - .../commerce/api/v0/tests/test_views.py | 2 - lms/djangoapps/commerce/api/v0/views.py | 5 - lms/djangoapps/email_marketing/admin.py | 9 - lms/djangoapps/email_marketing/apps.py | 5 - lms/djangoapps/email_marketing/signals.py | 320 --------- lms/djangoapps/email_marketing/tasks.py | 516 -------------- .../email_marketing/tests/__init__.py | 0 .../email_marketing/tests/test_signals.py | 669 ------------------ .../verify_student/tests/test_views.py | 9 +- lms/envs/production.py | 8 - openedx/core/djangoapps/user_authn/apps.py | 12 +- openedx/core/djangoapps/user_authn/signals.py | 43 ++ .../user_authn/tests/test_signals.py | 30 + .../features/enterprise_support/signals.py | 18 - .../enterprise_support/tests/test_signals.py | 14 - requirements/edx/base.in | 1 - 22 files changed, 87 insertions(+), 1586 deletions(-) delete mode 100644 lms/djangoapps/email_marketing/admin.py delete mode 100644 lms/djangoapps/email_marketing/signals.py delete mode 100644 lms/djangoapps/email_marketing/tasks.py delete mode 100644 lms/djangoapps/email_marketing/tests/__init__.py delete mode 100644 lms/djangoapps/email_marketing/tests/test_signals.py create mode 100644 openedx/core/djangoapps/user_authn/signals.py create mode 100644 openedx/core/djangoapps/user_authn/tests/test_signals.py diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 119a3a2f83..0bd47028ba 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -93,8 +93,6 @@ log = logging.getLogger(__name__) AUDIT_LOG = logging.getLogger("audit") 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: diff --git a/common/djangoapps/student/signals/__init__.py b/common/djangoapps/student/signals/__init__.py index 1fab564834..0ae2390011 100644 --- a/common/djangoapps/student/signals/__init__.py +++ b/common/djangoapps/student/signals/__init__.py @@ -4,6 +4,5 @@ from common.djangoapps.student.signals.signals import ( ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED, REFUND_ORDER, - SAILTHRU_AUDIT_PURCHASE, UNENROLL_DONE ) diff --git a/common/djangoapps/student/signals/signals.py b/common/djangoapps/student/signals/signals.py index 41a10d0dad..c7c2cd44cd 100644 --- a/common/djangoapps/student/signals/signals.py +++ b/common/djangoapps/student/signals/signals.py @@ -9,4 +9,3 @@ ENROLLMENT_TRACK_UPDATED = Signal(providing_args=['user', 'course_key', 'mode', UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"]) ENROLL_STATUS_CHANGE = Signal(providing_args=["event", "user", "course_id", "mode", "cost", "currency"]) REFUND_ORDER = Signal(providing_args=["course_enrollment"]) -SAILTHRU_AUDIT_PURCHASE = Signal(providing_args=["user", "course_id", "mode"]) diff --git a/common/djangoapps/util/model_utils.py b/common/djangoapps/util/model_utils.py index 8cdbb7fa6e..a193170ada 100644 --- a/common/djangoapps/util/model_utils.py +++ b/common/djangoapps/util/model_utils.py @@ -12,7 +12,6 @@ from eventtracking import tracker # The setting name used for events when "settings" (account settings, preferences, profile information) change. USER_SETTINGS_CHANGED_EVENT_NAME = 'edx.user.settings.changed' # Used to signal a field value change -USER_FIELD_CHANGED = Signal(providing_args=["user", "table", "setting", "old_value", "new_value"]) USER_FIELDS_CHANGED = Signal(providing_args=["user", "table", "changed_values"]) @@ -153,10 +152,6 @@ def emit_settings_changed_event(user, db_table, changed_fields: Dict[str, Tuple[ 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) - # Announce field change USER_FIELDS_CHANGED.send(sender=None, user=user, table=db_table, changed_fields=changed_fields) diff --git a/docs/decisions/0007-sys-path-modification-removal.rst b/docs/decisions/0007-sys-path-modification-removal.rst index 7c62ad9956..8a94a4a45d 100644 --- a/docs/decisions/0007-sys-path-modification-removal.rst +++ b/docs/decisions/0007-sys-path-modification-removal.rst @@ -129,8 +129,6 @@ What the old imports are, and their replacements: +-------------------------------+----------------------------------------------+ | ``edxnotes`` | ``lms.djangoapps.edxnotes`` | +-------------------------------+----------------------------------------------+ -| ``email_marketing`` | ``lms.djangoapps.email_marketing`` | -+-------------------------------+----------------------------------------------+ | ``experiments`` | ``lms.djangoapps.experiments`` | +-------------------------------+----------------------------------------------+ | ``gating`` | ``lms.djangoapps.gating`` | diff --git a/docs/guides/docstrings/lms_index.rst b/docs/guides/docstrings/lms_index.rst index 32df0e0b58..108a65cd39 100644 --- a/docs/guides/docstrings/lms_index.rst +++ b/docs/guides/docstrings/lms_index.rst @@ -14,7 +14,6 @@ Studio. lms/djangoapps/bulk_email/modules lms/djangoapps/courseware/modules lms/djangoapps/coursewarehistoryextended/modules - lms/djangoapps/email_marketing/modules lms/djangoapps/experiments/modules lms/djangoapps/lti_provider/modules lms/djangoapps/mobile_api/modules diff --git a/lms/djangoapps/commerce/api/v0/tests/test_views.py b/lms/djangoapps/commerce/api/v0/tests/test_views.py index ba4765a5ae..dfcadd03b3 100644 --- a/lms/djangoapps/commerce/api/v0/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v0/tests/test_views.py @@ -28,7 +28,6 @@ from xmodule.modulestore.tests.factories import CourseFactory from ....constants import Messages from ....tests.mocks import mock_basket_order from ....tests.test_views import UserMixin -from ..views import SAILTHRU_CAMPAIGN_COOKIE UTM_COOKIE_NAME = 'edx.test.utm' UTM_COOKIE_CONTENTS = { @@ -57,7 +56,6 @@ class BasketsViewTests(UserMixin, ModuleStoreTestCase): if marketing_email_opt_in: payload["email_opt_in"] = True - self.client.cookies[SAILTHRU_CAMPAIGN_COOKIE] = 'sailthru id' if include_utm_cookie: self.client.cookies[UTM_COOKIE_NAME] = json.dumps(UTM_COOKIE_CONTENTS) return self.client.post(self.url, payload) diff --git a/lms/djangoapps/commerce/api/v0/views.py b/lms/djangoapps/commerce/api/v0/views.py index fb9f348dd4..a8fd9313de 100644 --- a/lms/djangoapps/commerce/api/v0/views.py +++ b/lms/djangoapps/commerce/api/v0/views.py @@ -16,7 +16,6 @@ from rest_framework.views import APIView from common.djangoapps.course_modes.models import CourseMode from common.djangoapps.entitlements.models import CourseEntitlement from common.djangoapps.student.models import CourseEnrollment -from common.djangoapps.student.signals import SAILTHRU_AUDIT_PURCHASE from common.djangoapps.util.json_request import JsonResponse from lms.djangoapps.courseware import courses from openedx.core.djangoapps.commerce.utils import ecommerce_api_client @@ -30,7 +29,6 @@ from ...constants import Messages from ...http import DetailResponse log = logging.getLogger(__name__) -SAILTHRU_CAMPAIGN_COOKIE = 'sailthru_bid' class BasketsView(APIView): @@ -153,9 +151,6 @@ class BasketsView(APIView): log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR - SAILTHRU_AUDIT_PURCHASE.send( - sender=None, user=user, mode=mode, course_id=course_id - ) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) else: diff --git a/lms/djangoapps/email_marketing/admin.py b/lms/djangoapps/email_marketing/admin.py deleted file mode 100644 index 300523b6c6..0000000000 --- a/lms/djangoapps/email_marketing/admin.py +++ /dev/null @@ -1,9 +0,0 @@ -""" Admin site bindings for email marketing """ - - -from config_models.admin import ConfigurationModelAdmin -from django.contrib import admin - -from .models import EmailMarketingConfiguration - -admin.site.register(EmailMarketingConfiguration, ConfigurationModelAdmin) diff --git a/lms/djangoapps/email_marketing/apps.py b/lms/djangoapps/email_marketing/apps.py index 17118c8696..bf78636d49 100644 --- a/lms/djangoapps/email_marketing/apps.py +++ b/lms/djangoapps/email_marketing/apps.py @@ -2,7 +2,6 @@ Configuration for the email_marketing Django application. """ - from django.apps import AppConfig @@ -12,7 +11,3 @@ class EmailMarketingConfig(AppConfig): """ name = 'lms.djangoapps.email_marketing' verbose_name = "Email Marketing" - - def ready(self): - # Register the signal handlers. - from . import signals # lint-amnesty, pylint: disable=unused-import, unused-variable diff --git a/lms/djangoapps/email_marketing/signals.py b/lms/djangoapps/email_marketing/signals.py deleted file mode 100644 index 0a3dc7a2c1..0000000000 --- a/lms/djangoapps/email_marketing/signals.py +++ /dev/null @@ -1,320 +0,0 @@ -""" -This module contains signals needed for email integration -""" - - -import datetime -import logging -from random import randint -from typing import Any, Dict, Optional, Tuple - -import crum -from celery.exceptions import TimeoutError as CeleryTimeoutError -from django.conf import settings -from django.dispatch import receiver -from edx_toggles.toggles import LegacyWaffleSwitchNamespace -from sailthru.sailthru_error import SailthruClientError - -from common.djangoapps import third_party_auth -from common.djangoapps.course_modes.models import CourseMode -from common.djangoapps.student.helpers import does_user_profile_exist -from common.djangoapps.student.models import UserProfile -from common.djangoapps.student.signals import SAILTHRU_AUDIT_PURCHASE -from common.djangoapps.track import segment -from common.djangoapps.util.model_utils import USER_FIELD_CHANGED, USER_FIELDS_CHANGED -from lms.djangoapps.email_marketing.tasks import ( - get_email_cookies_via_sailthru, - update_course_enrollment, - update_user, - update_user_email -) -from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY -from openedx.core.djangoapps.user_authn.cookies import CREATE_LOGON_COOKIE -from openedx.core.djangoapps.user_authn.views.register import REGISTER_USER - -from .models import EmailMarketingConfiguration - -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', LANGUAGE_KEY] - -# TODO: Remove in AA-607 -WAFFLE_NAMESPACE = 'sailthru' -WAFFLE_SWITCHES = LegacyWaffleSwitchNamespace(name=WAFFLE_NAMESPACE) - -# TODO: Remove in AA-607 -SAILTHRU_AUDIT_PURCHASE_ENABLED = 'audit_purchase_enabled' - - -# TODO: Remove in AA-607 -@receiver(SAILTHRU_AUDIT_PURCHASE) -def update_sailthru(sender, user, mode, course_id, **kwargs): # pylint: disable=unused-argument - """ - Receives signal and calls a celery task to update the - enrollment track - Arguments: - user: current user - course_id: course key of a course - Returns: - None - """ - if WAFFLE_SWITCHES.is_enabled(SAILTHRU_AUDIT_PURCHASE_ENABLED) and mode in CourseMode.AUDIT_MODES: - email = user.email.encode('utf-8') - update_course_enrollment.delay(email, course_id, mode, site=_get_current_site()) - - -# TODO: Remove in AA-607 -@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 - - post_parms = { - 'id': user.email, - 'fields': {'keys': 1}, - 'vars': {'last_login_date': datetime.datetime.now().strftime("%Y-%m-%d")} - } - - # get anonymous_interest cookie to capture usage before logon - request = crum.get_current_request() - if request: - sailthru_content = request.COOKIES.get('anonymous_interest') - if sailthru_content: - post_parms['cookies'] = {'anonymous_interest': sailthru_content} - - time_before_call = datetime.datetime.now() - sailthru_response = get_email_cookies_via_sailthru.delay(user.email, post_parms) - - try: - # synchronous call to get result of an asynchronous celery task, with timeout - sailthru_response.get(timeout=email_config.user_registration_cookie_timeout_delay, - propagate=True) - cookie = sailthru_response.result - _log_sailthru_api_call_time(time_before_call) - - except CeleryTimeoutError as exc: - log.error("Timeout error while attempting to obtain cookie from Sailthru: %s", str(exc)) - return response - except SailthruClientError as exc: - log.error("Exception attempting to obtain cookie from Sailthru: %s", str(exc)) - return response - except Exception: # lint-amnesty, pylint: disable=broad-except - log.error("Exception Connecting to celery task for %s", user.email) - return response - - if not cookie: - log.error("No cookie returned attempting to obtain cookie from Sailthru for %s", user.email) - return response - else: - response.set_cookie( - 'sailthru_hid', - cookie, - max_age=365 * 24 * 60 * 60, # set for 1 year - domain=settings.SESSION_COOKIE_DOMAIN, - path='/', - secure=request.is_secure() - ) - log.info("sailthru_hid cookie:%s successfully retrieved for user %s", cookie, user.email) - - return response - - -# TODO: Remove in AA-607 -@receiver(REGISTER_USER) -def email_marketing_register_user(sender, user, registration, - **kwargs): # pylint: disable=unused-argument - """ - Called after user created and saved - - Args: - sender: Not used - user: The user object for the user being changed - registration: The user registration profile to activate user account - kwargs: Not used - """ - email_config = EmailMarketingConfiguration.current() - if not email_config.enabled: - return - - # ignore anonymous users - if user.is_anonymous: - return - - # perform update asynchronously - update_user.delay(_create_sailthru_user_vars(user, user.profile, registration=registration), user.email, - site=_get_current_site(), new_user=True) - - -# TODO: Remove in AA-607 -@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 - """ - - # ignore anonymous users - if user.is_anonymous: - return - - # Ignore users that do not yet have a profile - if not does_user_profile_exist(user): - return - - # ignore anything but User, Profile or UserPreference tables - if table not in {'auth_user', 'auth_userprofile', 'user_api_userpreference'}: - return - - # ignore anything not in list of fields to handle - if setting in CHANGED_FIELDNAMES: - # skip if not enabled - # the check has to be here rather than at the start of the method to avoid - # accessing the config during migration 0001_date__add_ecommerce_service_user - email_config = EmailMarketingConfiguration.current() - if not email_config.enabled: - return - - # Is the status of the user account changing to active? - is_activation = (setting == 'is_active') and new_value is True - - # Is this change in the context of an SSO-initiated registration? - third_party_provider = None - if third_party_auth.is_enabled(): - running_pipeline = third_party_auth.pipeline.get(crum.get_current_request()) - if running_pipeline: - third_party_provider = third_party_auth.provider.Registry.get_from_pipeline(running_pipeline) - - # Send a welcome email if the user account is being activated - # and we are not in a SSO registration flow whose associated - # identity provider is configured to allow for the sending - # of a welcome email. - send_welcome_email = is_activation and ( - third_party_provider is None or third_party_provider.send_welcome_email - ) - - # set the activation flag when the user is marked as activated - update_user.delay(_create_sailthru_user_vars(user, user.profile), user.email, site=_get_current_site(), - new_user=False, activation=send_welcome_email) - - elif setting == 'email': - # email update is special case - email_config = EmailMarketingConfiguration.current() - if not email_config.enabled: - return - update_user_email.delay(user.email, old_value) - - -@receiver(USER_FIELDS_CHANGED) -def email_marketing_user_fields_changed( - sender, # pylint: disable=unused-argument - user=None, - table=None, - changed_fields: Optional[Dict[str, Tuple[Any, Any]]] = None, - **kwargs -): - """ - Update a collection of user profile fields - - Args: - sender: Not used - user: The user object for the user being changed - table: The name of the table being updated - changed_fields: A mapping from changed field name to old and new values. - kwargs: Not used - """ - - fields = {field: new_value for (field, (old_value, new_value)) in changed_fields.items()} - # This mirrors the logic in openedx/core/djangoapps/user_authn/views/register.py:_track_user_registration - if table == 'auth_userprofile': - if 'gender' in fields and fields['gender']: - fields['gender'] = dict(UserProfile.GENDER_CHOICES)[fields['gender']] - if 'country' in fields: - fields['country'] = str(fields['country']) - if 'level_of_education' in fields and fields['level_of_education']: - fields['education'] = dict(UserProfile.LEVEL_OF_EDUCATION_CHOICES)[fields['level_of_education']] - if 'year_of_birth' in fields: - fields['yearOfBirth'] = fields.pop('year_of_birth') - if 'mailing_address' in fields: - fields['address'] = fields.pop('mailing_address') - - segment.identify( - user.id, - fields - ) - - -def _create_sailthru_user_vars(user, profile, registration=None): - """ - Create sailthru user create/update vars from user + profile. - """ - sailthru_vars = {'username': user.username, - 'activated': int(user.is_active), - 'joined_date': user.date_joined.strftime("%Y-%m-%d")} - - # Set the ui_lang to the User's prefered language, if specified. Otherwise use the application's default language. - sailthru_vars['ui_lang'] = user.preferences.model.get_value(user, LANGUAGE_KEY, default=settings.LANGUAGE_CODE) - - if profile: - sailthru_vars['fullname'] = profile.name - sailthru_vars['gender'] = profile.gender - sailthru_vars['education'] = profile.level_of_education - - if profile.year_of_birth: - sailthru_vars['year_of_birth'] = profile.year_of_birth - sailthru_vars['country'] = str(profile.country.code) - - if registration: - sailthru_vars['activation_key'] = registration.activation_key - sailthru_vars['signupNumber'] = randint(0, 9) - - return sailthru_vars - - -def _get_current_site(): - """ - Returns the site for the current request if any. - """ - request = crum.get_current_request() - if not request: - return - - return {'id': request.site.id, 'domain': request.site.domain, 'name': request.site.name} - - -def _log_sailthru_api_call_time(time_before_call): - """ - Logs Sailthru api synchronous call time - """ - - time_after_call = datetime.datetime.now() - delta_sailthru_api_call_time = time_after_call - time_before_call - - log.info("Started at %s and ended at %s, time spent:%s milliseconds", - time_before_call.isoformat(' '), - time_after_call.isoformat(' '), - delta_sailthru_api_call_time.microseconds / 1000) diff --git a/lms/djangoapps/email_marketing/tasks.py b/lms/djangoapps/email_marketing/tasks.py deleted file mode 100644 index 29ee7bc76d..0000000000 --- a/lms/djangoapps/email_marketing/tasks.py +++ /dev/null @@ -1,516 +0,0 @@ -""" -This file contains celery tasks for email marketing signal handler. -""" - - -import logging -import time -from datetime import datetime, timedelta - -from celery import shared_task -from django.conf import settings -from django.core.cache import cache -from edx_django_utils.monitoring import set_code_owner_attribute -from sailthru.sailthru_client import SailthruClient -from sailthru.sailthru_error import SailthruClientError - -from .models import EmailMarketingConfiguration - -log = logging.getLogger(__name__) -SAILTHRU_LIST_CACHE_KEY = "email.marketing.cache" - - -# TODO: Remove in AA-607 -@shared_task(bind=True) -@set_code_owner_attribute -def get_email_cookies_via_sailthru(self, user_email, post_parms): # lint-amnesty, pylint: disable=unused-argument - """ - Adds/updates Sailthru cookie information for a new user. - Args: - post_parms(dict): User profile information to pass as 'vars' to Sailthru - Returns: - cookie(str): cookie fetched from Sailthru - """ - - email_config = EmailMarketingConfiguration.current() - if not email_config.enabled: - return None - - try: - sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) - log.info( - 'Sending to Sailthru the user interest cookie [%s] for user [%s]', - post_parms.get('cookies', ''), - user_email - ) - sailthru_response = sailthru_client.api_post("user", post_parms) - except SailthruClientError as exc: - log.error("Exception attempting to obtain cookie from Sailthru: %s", str(exc)) - raise SailthruClientError # lint-amnesty, pylint: disable=raise-missing-from - - if sailthru_response.is_ok(): - if 'keys' in sailthru_response.json and 'cookie' in sailthru_response.json['keys']: - cookie = sailthru_response.json['keys']['cookie'] - return cookie - else: - log.error("No cookie returned attempting to obtain cookie from Sailthru for %s", user_email) - else: - error = sailthru_response.get_error() - # generally invalid email address - log.info("Error attempting to obtain cookie from Sailthru: %s", error.get_message()) - - return None - - -# TODO: Remove in AA-607 -@shared_task(bind=True, default_retry_delay=3600, max_retries=24) -@set_code_owner_attribute -def update_user(self, sailthru_vars, email, site=None, new_user=False, activation=False): - """ - Adds/updates Sailthru profile information for a user. - Args: - sailthru_vars(dict): User profile information to pass as 'vars' to Sailthru - email(str): User email address - new_user(boolean): True if new registration - activation(boolean): True if a welcome email should be sent - Returns: - None - """ - email_config = EmailMarketingConfiguration.current() - if not email_config.enabled: - return - - # do not add user if registered at a white label site - if not is_default_site(site): - return - - sailthru_client = SailthruClient(email_config.sailthru_key, email_config.sailthru_secret) - try: - sailthru_response = sailthru_client.api_post("user", - _create_email_user_param(sailthru_vars, sailthru_client, - email, new_user, email_config, - site=site)) - - except SailthruClientError as exc: - log.error("Exception attempting to add/update user %s in Sailthru - %s", email, str(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 add/update user in Sailthru: %s", error.get_message()) - if _retryable_sailthru_error(error): - raise self.retry(countdown=email_config.sailthru_retry_interval, - max_retries=email_config.sailthru_max_retries) - return - - if activation and email_config.sailthru_welcome_template and not sailthru_vars.get('is_enterprise_learner'): - - scheduled_datetime = datetime.utcnow() + timedelta(seconds=email_config.welcome_email_send_delay) - try: - sailthru_response = sailthru_client.api_post( - "send", - { - "email": email, - "template": email_config.sailthru_welcome_template, - "schedule_time": scheduled_datetime.strftime('%Y-%m-%dT%H:%M:%SZ') - } - ) - except SailthruClientError as exc: - log.error( - "Exception attempting to send welcome email to user %s in Sailthru - %s", - email, - str(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 send welcome email to user in Sailthru: %s", error.get_message()) - if _retryable_sailthru_error(error): - raise self.retry(countdown=email_config.sailthru_retry_interval, - max_retries=email_config.sailthru_max_retries) - - -def is_default_site(site): - """ - Checks whether the site is a default site or a white-label - Args: - site: A dict containing the site info - Returns: - Boolean - """ - return not site or site.get('id') == settings.SITE_ID - - -# TODO: Remove in AA-607 -@shared_task(bind=True, default_retry_delay=3600, max_retries=24) -@set_code_owner_attribute -def update_user_email(self, new_email, 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 - - # ignore if email not changed - if new_email == old_email: - return - - sailthru_parms = {"id": old_email, "key": "email", "keysconflict": "merge", "keys": {"email": new_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", old_email, str(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()) - if _retryable_sailthru_error(error): - raise self.retry(countdown=email_config.sailthru_retry_interval, - max_retries=email_config.sailthru_max_retries) - - -def _create_email_user_param(sailthru_vars, sailthru_client, email, new_user, email_config, site=None): - """ - Create sailthru user create/update parms - """ - sailthru_user = {'id': email, 'key': 'email'} - sailthru_user['vars'] = dict(sailthru_vars, last_changed_time=int(time.time())) - - # if new user add to list - if new_user: - list_name = _get_or_create_user_list_for_site( - sailthru_client, site=site, default_list_name=email_config.sailthru_new_user_list - ) - sailthru_user['lists'] = {list_name: 1} if list_name else {email_config.sailthru_new_user_list: 1} - - return sailthru_user - - -# TODO: Remove in AA-607 -def _get_or_create_user_list_for_site(sailthru_client, site=None, default_list_name=None): - """ - Get the user list name from cache if exists else create one and return the name, - callers of this function should perform the enabled check of email config. - :param: sailthru_client - :param: site - :param: default_list_name - :return: list name if exists or created else return None - """ - if not is_default_site(site): - list_name = site.get('domain', '').replace(".", "_") + "_user_list" - else: - list_name = default_list_name - - sailthru_list = _get_or_create_user_list(sailthru_client, list_name) - return list_name if sailthru_list else default_list_name - - -# TODO: Remove in AA-607 -def _get_or_create_user_list(sailthru_client, list_name): - """ - Get list from sailthru and return if list_name exists else create a new one - and return list data for all lists. - :param sailthru_client - :param list_name - :return sailthru list - """ - sailthru_list_cache = cache.get(SAILTHRU_LIST_CACHE_KEY) - is_cache_updated = False - if not sailthru_list_cache: - sailthru_list_cache = _get_list_from_email_marketing_provider(sailthru_client) - is_cache_updated = True - - sailthru_list = sailthru_list_cache.get(list_name) - - if not sailthru_list: - is_created = _create_user_list(sailthru_client, list_name) - if is_created: - sailthru_list_cache = _get_list_from_email_marketing_provider(sailthru_client) - is_cache_updated = True - sailthru_list = sailthru_list_cache.get(list_name) - - if is_cache_updated: - cache.set(SAILTHRU_LIST_CACHE_KEY, sailthru_list_cache) - - return sailthru_list - - -# TODO: Remove in AA-607 -def _get_list_from_email_marketing_provider(sailthru_client): - """ - Get sailthru list - :param sailthru_client - :return dict of sailthru lists mapped by list name - """ - try: - sailthru_get_response = sailthru_client.api_get("list", {}) - except SailthruClientError as exc: - log.error("Exception attempting to get list from Sailthru - %s", str(exc)) - return {} - - if not sailthru_get_response.is_ok(): - error = sailthru_get_response.get_error() - log.info("Error attempting to read list record from Sailthru: %s", error.get_message()) - return {} - - list_map = dict() - for user_list in sailthru_get_response.json['lists']: - list_map[user_list.get('name')] = user_list - - return list_map - - -def _create_user_list(sailthru_client, list_name): - """ - Create list in Sailthru - :param sailthru_client - :param list_name - :return boolean - """ - list_params = {'list': list_name, 'primary': 0, 'public_name': list_name} - try: - sailthru_response = sailthru_client.api_post("list", list_params) - except SailthruClientError as exc: - log.error("Exception attempting to list record for key %s in Sailthru - %s", list_name, str(exc)) - return False - - if not sailthru_response.is_ok(): - error = sailthru_response.get_error() - log.error("Error attempting to create list in Sailthru: %s", error.get_message()) - return False - - return True - - -# TODO: Remove in AA-607 -def _retryable_sailthru_error(error): - """ Return True if error should be retried. - - 9: Retryable internal error - 43: Rate limiting response - others: Not retryable - - See: https://getstarted.sailthru.com/new-for-developers-overview/api/api-response-errors/ - """ - code = error.get_error_code() - return code == 9 or code == 43 # lint-amnesty, pylint: disable=consider-using-in - - -# TODO: Remove in AA-607 -@shared_task(bind=True) -@set_code_owner_attribute -def update_course_enrollment(self, email, course_key, mode, site=None): - """Adds/updates Sailthru when a user adds to cart/purchases/upgrades a course - Args: - email: email address of enrolled user - course_key: course key of course - mode: mode user is enrolled in - site: site where user enrolled - Returns: - None - """ - # do not add user if registered at a white label site - if not is_default_site(site): - return - - course_url = build_course_url(course_key) - config = EmailMarketingConfiguration.current() - - try: - sailthru_client = SailthruClient(config.sailthru_key, config.sailthru_secret) - except: # lint-amnesty, pylint: disable=bare-except - return - - send_template = config.sailthru_enroll_template - cost_in_cents = 0 - - if not update_unenrolled_list(sailthru_client, email, course_url, False): - schedule_retry(self, config) - - course_data = _get_course_content(course_key, course_url, sailthru_client, config) - - item = _build_purchase_item(course_key, course_url, cost_in_cents, mode, course_data) - options = {} - - if send_template: - options['send_template'] = send_template - - if not _record_purchase(sailthru_client, email, item, options): - schedule_retry(self, config) - - -def build_course_url(course_key): - """ - Generates and return url of the course info page by using course_key - Arguments: - course_key: course_key of the given course - Returns - a complete url of the course info page - """ - return '{base_url}/courses/{course_key}/info'.format(base_url=settings.LMS_ROOT_URL, - course_key=str(course_key)) - - -# TODO: Remove in AA-607 -def update_unenrolled_list(sailthru_client, email, course_url, unenroll): - """Maintain a list of courses the user has unenrolled from in the Sailthru user record - Arguments: - sailthru_client: SailthruClient - email (str): user's email address - course_url (str): LMS url for course info page. - unenroll (boolean): True if unenrolling, False if enrolling - Returns: - False if retryable error, else True - """ - try: - # get the user 'vars' values from sailthru - sailthru_response = sailthru_client.api_get("user", {"id": email, "fields": {"vars": 1}}) - if not sailthru_response.is_ok(): - error = sailthru_response.get_error() - log.error("Error attempting to read user record from Sailthru: %s", error.get_message()) - return not _retryable_sailthru_error(error) - - response_json = sailthru_response.json - - unenroll_list = [] - if response_json and "vars" in response_json and response_json["vars"] \ - and "unenrolled" in response_json["vars"]: - unenroll_list = response_json["vars"]["unenrolled"] - - changed = False - # if unenrolling, add course to unenroll list - if unenroll: - if course_url not in unenroll_list: - unenroll_list.append(course_url) - changed = True - - # if enrolling, remove course from unenroll list - elif course_url in unenroll_list: - unenroll_list.remove(course_url) - changed = True - - if changed: - # write user record back - sailthru_response = sailthru_client.api_post( - 'user', {'id': email, 'key': 'email', 'vars': {'unenrolled': unenroll_list}}) - - if not sailthru_response.is_ok(): - error = sailthru_response.get_error() - log.error("Error attempting to update user record in Sailthru: %s", error.get_message()) - return not _retryable_sailthru_error(error) - - return True - - except SailthruClientError as exc: - log.exception("Exception attempting to update user record for %s in Sailthru - %s", email, str(exc)) - return False - - -# TODO: Remove in AA-607 -def schedule_retry(self, config): - """Schedule a retry""" - raise self.retry(countdown=config.sailthru_retry_interval, - max_retries=config.sailthru_max_retries) - - -# TODO: Remove in AA-607 -def _get_course_content(course_id, course_url, sailthru_client, config): - """Get course information using the Sailthru content api or from cache. - If there is an error, just return with an empty response. - Arguments: - course_id (str): course key of the course - course_url (str): LMS url for course info page. - sailthru_client : SailthruClient - config : config options - Returns: - course information from Sailthru - """ - # check cache first - - cache_key = f"{course_id}:{course_url}" - response = cache.get(cache_key) - if not response: - try: - sailthru_response = sailthru_client.api_get("content", {"id": course_url}) - if not sailthru_response.is_ok(): - log.error('Could not get course data from Sailthru on enroll/unenroll event. ') - response = {} - else: - response = sailthru_response.json - cache.set(cache_key, response, config.sailthru_content_cache_age) - - except SailthruClientError: - response = {} - - return response - - -# TODO: Remove in AA-607 -def _build_purchase_item(course_id, course_url, cost_in_cents, mode, course_data): - """Build and return Sailthru purchase item object""" - - # build item description - item = { - 'id': f"{course_id}-{mode}", - 'url': course_url, - 'price': cost_in_cents, - 'qty': 1, - } - - # get title from course info if we don't already have it from Sailthru - if 'title' in course_data: - item['title'] = course_data['title'] - else: - # can't find, just invent title - item['title'] = f'Course {course_id} mode: {mode}' - - if 'tags' in course_data: - item['tags'] = course_data['tags'] - - # add vars to item - item['vars'] = dict(course_data.get('vars', {}), mode=mode, course_run_id=str(course_id)) - - return item - - -# TODO: Remove in AA-607 -def _record_purchase(sailthru_client, email, item, options): - """ - Record a purchase in Sailthru - Arguments: - sailthru_client: SailthruClient - email: user's email address - item: Sailthru required information - options: Sailthru purchase API options - Returns: - False if retryable error, else True - """ - - try: - sailthru_response = sailthru_client.purchase(email, [item], options=options) - - if not sailthru_response.is_ok(): - error = sailthru_response.get_error() - log.error("Error attempting to record purchase in Sailthru: %s", error.get_message()) - return not _retryable_sailthru_error(error) - - except SailthruClientError as exc: - log.exception("Exception attempting to record purchase for %s in Sailthru - %s", email, str(exc)) - return False - return True diff --git a/lms/djangoapps/email_marketing/tests/__init__.py b/lms/djangoapps/email_marketing/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/email_marketing/tests/test_signals.py b/lms/djangoapps/email_marketing/tests/test_signals.py deleted file mode 100644 index 45a5305a48..0000000000 --- a/lms/djangoapps/email_marketing/tests/test_signals.py +++ /dev/null @@ -1,669 +0,0 @@ -"""Tests of email marketing signal handlers.""" - - -import datetime -import logging -from unittest.mock import ANY, Mock, patch - -import ddt -from django.conf import settings -from django.contrib.auth.models import AnonymousUser -from django.contrib.sites.models import Site -from django.test import TestCase -from django.test.client import RequestFactory -from freezegun import freeze_time -from opaque_keys.edx.keys import CourseKey -from sailthru.sailthru_error import SailthruClientError -from sailthru.sailthru_response import SailthruResponse -from testfixtures import LogCapture - -from common.djangoapps.student.models import Registration, User -from common.djangoapps.student.tests.factories import ( # lint-amnesty, pylint: disable=unused-import - CourseEnrollmentFactory, - UserFactory, - UserProfileFactory -) -from common.djangoapps.util.json_request import JsonResponse -from lms.djangoapps.email_marketing.tasks import ( # lint-amnesty, pylint: disable=unused-import - _create_user_list, - _get_list_from_email_marketing_provider, - _get_or_create_user_list, - get_email_cookies_via_sailthru, - update_user, - update_user_email -) -from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY - -from ..models import EmailMarketingConfiguration -from ..signals import ( - add_email_marketing_cookies, - email_marketing_register_user, - email_marketing_user_field_changed, - update_sailthru -) - -log = logging.getLogger(__name__) - -LOGGER_NAME = "lms.djangoapps.email_marketing.signals" - -TEST_EMAIL = "test@edx.org" - - -def update_email_marketing_config(enabled=True, key='badkey', secret='badsecret', new_user_list='new list', - template='Welcome', enroll_cost=100, lms_url_override='http://testserver'): - """ - Enable / Disable Sailthru integration - """ - return EmailMarketingConfiguration.objects.create( - enabled=enabled, - sailthru_key=key, - sailthru_secret=secret, - sailthru_new_user_list=new_user_list, - sailthru_welcome_template=template, - sailthru_enroll_template='enroll_template', - sailthru_lms_url_override=lms_url_override, - sailthru_get_tags_from_sailthru=False, - sailthru_enroll_cost=enroll_cost, - sailthru_max_retries=0, - welcome_email_send_delay=600 - ) - - -@ddt.ddt -class EmailMarketingTests(TestCase): - """ - Tests for the EmailMarketing signals and tasks classes. - """ - - def setUp(self): - update_email_marketing_config(enabled=False) - self.request_factory = RequestFactory() - self.user = UserFactory.create(username='test', email=TEST_EMAIL) - self.registration = Registration() - self.registration.register(self.user) - - self.request = self.request_factory.get("foo") - 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' - - self.site = Site.objects.get_current() - self.request.site = self.site - super().setUp() - - @freeze_time(datetime.datetime.now()) - @patch('lms.djangoapps.email_marketing.signals.crum.get_current_request') - @patch('sailthru.sailthru_client.SailthruClient.api_post') - def test_drop_cookie(self, mock_sailthru, mock_get_current_request): - """ - Test add_email_marketing_cookies - """ - response = JsonResponse({ - "success": True, - "redirect_url": 'test.com/test', - }) - self.request.COOKIES['anonymous_interest'] = 'cookie_content' - mock_get_current_request.return_value = self.request - - cookies = {'cookie': 'test_cookie'} - mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': cookies})) - - with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: - add_email_marketing_cookies(None, response=response, user=self.user) - logger.check( - (LOGGER_NAME, 'INFO', - 'Started at {start} and ended at {end}, time spent:{delta} milliseconds'.format( - start=datetime.datetime.now().isoformat(' '), - end=datetime.datetime.now().isoformat(' '), - delta=0.0) - ), - (LOGGER_NAME, 'INFO', - 'sailthru_hid cookie:{cookies[cookie]} successfully retrieved for user {user}'.format( - cookies=cookies, - user=TEST_EMAIL) - ) - ) - mock_sailthru.assert_called_with('user', - {'fields': {'keys': 1}, - 'cookies': {'anonymous_interest': 'cookie_content'}, - 'id': TEST_EMAIL, - 'vars': {'last_login_date': ANY}}) - assert 'sailthru_hid' in response.cookies - assert response.cookies['sailthru_hid'].value == 'test_cookie' - - @patch('sailthru.sailthru_client.SailthruClient.api_post') - def test_get_cookies_via_sailthu(self, mock_sailthru): - - cookies = {'cookie': 'test_cookie'} - mock_sailthru.return_value = SailthruResponse(JsonResponse({'keys': cookies})) - - post_parms = { - 'id': self.user.email, - 'fields': {'keys': 1}, - 'vars': {'last_login_date': datetime.datetime.now().strftime("%Y-%m-%d")}, - 'cookies': {'anonymous_interest': 'cookie_content'} - } - expected_cookie = get_email_cookies_via_sailthru.delay(self.user.email, post_parms) - - mock_sailthru.assert_called_with('user', - {'fields': {'keys': 1}, - 'cookies': {'anonymous_interest': 'cookie_content'}, - 'id': TEST_EMAIL, - 'vars': {'last_login_date': ANY}}) - - assert cookies['cookie'] == expected_cookie.result - - @patch('sailthru.sailthru_client.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) - assert 'sailthru_hid' not in response.cookies - - mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': "error", "errormsg": "errormsg"})) - add_email_marketing_cookies(None, response=response, user=self.user) - assert 'sailthru_hid' not in response.cookies - - mock_sailthru.side_effect = SailthruClientError - add_email_marketing_cookies(None, response=response, user=self.user) - assert 'sailthru_hid' not in response.cookies - - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_get') - def test_add_user(self, mock_sailthru_get, mock_sailthru_post, mock_log_error): - """ - test async method in tasks that actually updates Sailthru - """ - site_dict = {'id': self.site.id, 'domain': self.site.domain, 'name': self.site.name} - mock_sailthru_post.return_value = SailthruResponse(JsonResponse({'ok': True})) - mock_sailthru_get.return_value = SailthruResponse(JsonResponse({'lists': [{'name': 'new list'}], 'ok': True})) - update_user.delay( - {'gender': 'm', 'username': 'test', 'activated': 1}, TEST_EMAIL, site_dict, new_user=True - ) - assert not mock_log_error.called - assert mock_sailthru_post.call_args[0][0] == 'user' - userparms = mock_sailthru_post.call_args[0][1] - assert userparms['key'] == 'email' - assert userparms['id'] == TEST_EMAIL - assert userparms['vars']['gender'] == 'm' - assert userparms['vars']['username'] == 'test' - assert userparms['vars']['activated'] == 1 - assert userparms['lists']['new list'] == 1 - - @patch('lms.djangoapps.email_marketing.signals.get_email_cookies_via_sailthru.delay') - def test_drop_cookie_task_error(self, mock_email_cookies): - """ - Tests that task error is handled - """ - mock_email_cookies.return_value = {} - mock_email_cookies.get.side_effect = Exception - with LogCapture(LOGGER_NAME, level=logging.INFO) as logger: - add_email_marketing_cookies(None, response=None, user=self.user) - logger.check(( - LOGGER_NAME, 'ERROR', 'Exception Connecting to celery task for {}'.format( - self.user.email - ) - )) - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_email_not_sent_to_enterprise_learners(self, mock_sailthru_post): - """ - tests that welcome email is not sent to the enterprise learner - """ - mock_sailthru_post.return_value = SailthruResponse(JsonResponse({'ok': True})) - update_user.delay( - sailthru_vars={ - 'is_enterprise_learner': True, - 'enterprise_name': 'test name', - }, - email=self.user.email - ) - assert mock_sailthru_post.call_args[0][0] != 'send' - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_add_user_list_not_called_on_white_label_domain(self, mock_sailthru_post): - """ - test user is not added to Sailthru user lists if registered from a whitel labe site - """ - existing_site = Site.objects.create(domain='testwhitelabel.com', name='White Label') - site_dict = {'id': existing_site.id, 'domain': existing_site.domain, 'name': existing_site.name} - update_user.delay( - {'gender': 'm', 'username': 'test', 'activated': 1}, TEST_EMAIL, site=site_dict, new_user=True - ) - assert not mock_sailthru_post.called - - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_update_user_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.email) - assert mock_log_error.called - - # force Sailthru API exception - mock_log_error.reset_mock() - mock_sailthru.side_effect = SailthruClientError - update_user.delay({}, self.user.email) - assert mock_log_error.called - - # force Sailthru API exception on 2nd call - mock_log_error.reset_mock() - mock_sailthru.side_effect = [SailthruResponse(JsonResponse({'ok': True})), SailthruClientError] - update_user.delay({}, self.user.email, activation=True) - assert mock_log_error.called - - # force Sailthru API error return on 2nd call - mock_log_error.reset_mock() - mock_sailthru.side_effect = [SailthruResponse(JsonResponse({'ok': True})), - SailthruResponse(JsonResponse({'error': 100, 'errormsg': 'Got an error'}))] - update_user.delay({}, self.user.email, activation=True) - assert mock_log_error.called - - @patch('lms.djangoapps.email_marketing.tasks.update_user.retry') - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_update_user_error_retryable(self, mock_sailthru, mock_log_error, mock_retry): - """ - Ensure that retryable error is retried - """ - mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': 43, 'errormsg': 'Got an error'})) - update_user.delay({}, self.user.email) - assert mock_log_error.called - assert mock_retry.called - - @patch('lms.djangoapps.email_marketing.tasks.update_user.retry') - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_update_user_error_nonretryable(self, mock_sailthru, mock_log_error, mock_retry): - """ - Ensure that non-retryable error is not retried - """ - mock_sailthru.return_value = SailthruResponse(JsonResponse({'error': 1, 'errormsg': 'Got an error'})) - update_user.delay({}, self.user.email) - assert mock_log_error.called - assert not mock_retry.called - - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - def test_just_return_tasks(self, mock_sailthru, mock_log_error): - """ - Ensure that disabling Sailthru just returns - """ - update_email_marketing_config(enabled=False) - - update_user.delay(self.user.username, self.user.email) - assert not mock_log_error.called - assert not mock_sailthru.called - - update_user_email.delay(self.user.username, "newemail2@test.com") - assert not mock_log_error.called - assert not mock_sailthru.called - - update_email_marketing_config(enabled=True) - - @patch('lms.djangoapps.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) - - add_email_marketing_cookies(None) - assert not mock_log_error.called - - email_marketing_register_user(None, None, None) - assert not mock_log_error.called - - update_email_marketing_config(enabled=True) - - # test anonymous users - anon = AnonymousUser() - email_marketing_register_user(None, anon, None) - assert not mock_log_error.called - - email_marketing_user_field_changed(None, user=anon) - assert not mock_log_error.called - - user = User(username='test', email='test@example.com') - email_marketing_user_field_changed(None, user=user) - assert not mock_log_error.called - - @patch('lms.djangoapps.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})) - update_user_email.delay(TEST_EMAIL, "old@edx.org") - assert mock_sailthru.call_args[0][0] == 'user' - userparms = mock_sailthru.call_args[0][1] - assert userparms['key'] == 'email' - assert userparms['id'] == 'old@edx.org' - assert userparms['keys']['email'] == TEST_EMAIL - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_get_or_create_sailthru_list(self, mock_sailthru_client): - """ - Test the task the create sailthru lists. - """ - mock_sailthru_client.api_get.return_value = SailthruResponse(JsonResponse({'lists': []})) - _get_or_create_user_list(mock_sailthru_client, 'test1_user_list') - mock_sailthru_client.api_get.assert_called_with("list", {}) - mock_sailthru_client.api_post.assert_called_with( - "list", {'list': 'test1_user_list', 'primary': 0, 'public_name': 'test1_user_list'} - ) - - # test existing user list - mock_sailthru_client.api_get.return_value = \ - SailthruResponse(JsonResponse({'lists': [{'name': 'test1_user_list'}]})) - _get_or_create_user_list(mock_sailthru_client, 'test2_user_list') - mock_sailthru_client.api_get.assert_called_with("list", {}) - mock_sailthru_client.api_post.assert_called_with( - "list", {'list': 'test2_user_list', 'primary': 0, 'public_name': 'test2_user_list'} - ) - - # test get error from Sailthru - mock_sailthru_client.api_get.return_value = \ - SailthruResponse(JsonResponse({'error': 43, 'errormsg': 'Got an error'})) - assert _get_or_create_user_list(mock_sailthru_client, 'test1_user_list') is None - - # test post error from Sailthru - mock_sailthru_client.api_post.return_value = \ - SailthruResponse(JsonResponse({'error': 43, 'errormsg': 'Got an error'})) - mock_sailthru_client.api_get.return_value = SailthruResponse(JsonResponse({'lists': []})) - assert _get_or_create_user_list(mock_sailthru_client, 'test2_user_list') is None - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_get_sailthru_list_map_no_list(self, mock_sailthru_client): - """Test when no list returned from sailthru""" - mock_sailthru_client.api_get.return_value = SailthruResponse(JsonResponse({'lists': []})) - assert _get_list_from_email_marketing_provider(mock_sailthru_client) == {} - mock_sailthru_client.api_get.assert_called_with("list", {}) - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_get_sailthru_list_map_error(self, mock_sailthru_client): - """Test when error occurred while fetching data from sailthru""" - mock_sailthru_client.api_get.return_value = SailthruResponse( - JsonResponse({'error': 43, 'errormsg': 'Got an error'}) - ) - assert _get_list_from_email_marketing_provider(mock_sailthru_client) == {} - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_get_sailthru_list_map_exception(self, mock_sailthru_client): - """Test when exception raised while fetching data from sailthru""" - mock_sailthru_client.api_get.side_effect = SailthruClientError - assert _get_list_from_email_marketing_provider(mock_sailthru_client) == {} - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_get_sailthru_list(self, mock_sailthru_client): - """Test fetch list data from sailthru""" - mock_sailthru_client.api_get.return_value = \ - SailthruResponse(JsonResponse({'lists': [{'name': 'test1_user_list'}]})) - assert _get_list_from_email_marketing_provider(mock_sailthru_client) == { - 'test1_user_list': {'name': 'test1_user_list'} - } - mock_sailthru_client.api_get.assert_called_with("list", {}) - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_create_sailthru_list(self, mock_sailthru_client): - """Test create list in sailthru""" - mock_sailthru_client.api_post.return_value = SailthruResponse(JsonResponse({'ok': True})) - assert _create_user_list(mock_sailthru_client, 'test_list_name') is True - assert mock_sailthru_client.api_post.call_args[0][0] == 'list' - listparms = mock_sailthru_client.api_post.call_args[0][1] - assert listparms['list'] == 'test_list_name' - assert listparms['primary'] == 0 - assert listparms['public_name'] == 'test_list_name' - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_create_sailthru_list_error(self, mock_sailthru_client): - """Test error occurrence while creating sailthru list""" - mock_sailthru_client.api_post.return_value = SailthruResponse( - JsonResponse({'error': 43, 'errormsg': 'Got an error'}) - ) - assert _create_user_list(mock_sailthru_client, 'test_list_name') is False - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient') - def test_create_sailthru_list_exception(self, mock_sailthru_client): - """Test exception raised while creating sailthru list""" - mock_sailthru_client.api_post.side_effect = SailthruClientError - assert _create_user_list(mock_sailthru_client, 'test_list_name') is False - - @patch('lms.djangoapps.email_marketing.tasks.log.error') - @patch('lms.djangoapps.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_email.delay(self.user.username, "newemail2@test.com") - assert mock_log_error.called - - mock_sailthru.side_effect = SailthruClientError - update_user_email.delay(self.user.username, "newemail2@test.com") - assert mock_log_error.called - - @patch('lms.djangoapps.email_marketing.signals.crum.get_current_request') - @patch('lms.djangoapps.email_marketing.tasks.update_user.delay') - def test_register_user(self, mock_update_user, mock_get_current_request): - """ - make sure register user call invokes update_user and includes activation_key - """ - mock_get_current_request.return_value = self.request - email_marketing_register_user(None, user=self.user, registration=self.registration) - assert mock_update_user.called - assert mock_update_user.call_args[0][0]['activation_key'] == self.registration.activation_key - assert mock_update_user.call_args[0][0]['signupNumber'] <= 9 - - @patch('lms.djangoapps.email_marketing.tasks.update_user.delay') - def test_register_user_no_request(self, mock_update_user): - """ - make sure register user call invokes update_user and includes activation_key - """ - email_marketing_register_user(None, user=self.user, registration=self.registration) - assert mock_update_user.called - assert mock_update_user.call_args[0][0]['activation_key'] == self.registration.activation_key - - @patch('lms.djangoapps.email_marketing.tasks.update_user.delay') - def test_register_user_language_preference(self, mock_update_user): - """ - make sure register user call invokes update_user and includes language preference - """ - # If the user hasn't set an explicit language preference, we should send the application's default. - assert self.user.preferences.model.get_value(self.user, LANGUAGE_KEY) is None - email_marketing_register_user(None, user=self.user, registration=self.registration) - assert mock_update_user.call_args[0][0]['ui_lang'] == settings.LANGUAGE_CODE - - # If the user has set an explicit language preference, we should send it. - self.user.preferences.create(key=LANGUAGE_KEY, value='es-419') - email_marketing_register_user(None, user=self.user, registration=self.registration) - assert mock_update_user.call_args[0][0]['ui_lang'] == 'es-419' - - @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) - @patch('lms.djangoapps.email_marketing.signals.crum.get_current_request') - @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), - ('user_api_userpreference', 'pref-lang', 'en', True)) - @ddt.unpack - def test_modify_field(self, table, setting, value, result, mock_update_user, mock_get_current_request): - """ - Test that correct fields call update_user - """ - mock_get_current_request.return_value = self.request - email_marketing_user_field_changed(None, self.user, table=table, setting=setting, new_value=value) - assert mock_update_user.called == result - - @patch('lms.djangoapps.email_marketing.tasks.SailthruClient.api_post') - @patch('lms.djangoapps.email_marketing.signals.third_party_auth.provider.Registry.get_from_pipeline') - @patch('lms.djangoapps.email_marketing.signals.third_party_auth.pipeline.get') - @patch('lms.djangoapps.email_marketing.signals.crum.get_current_request') - @ddt.data(True, False) - def test_modify_field_with_sso(self, send_welcome_email, mock_get_current_request, - mock_pipeline_get, mock_registry_get_from_pipeline, mock_sailthru_post): - """ - Test that welcome email is sent appropriately in the context of SSO registration - """ - mock_get_current_request.return_value = self.request - mock_pipeline_get.return_value = 'saml-idp' - mock_registry_get_from_pipeline.return_value = Mock(send_welcome_email=send_welcome_email) - mock_sailthru_post.return_value = SailthruResponse(JsonResponse({'ok': True})) - email_marketing_user_field_changed(None, self.user, table='auth_user', setting='is_active', new_value=True) - if send_welcome_email: - assert mock_sailthru_post.call_args[0][0] == 'send' - else: - assert mock_sailthru_post.call_args[0][0] != 'send' - - @patch('lms.djangoapps.email_marketing.tasks.update_user.delay') - def test_modify_language_preference(self, mock_update_user): - """ - Test that update_user is called with new language preference - """ - # If the user hasn't set an explicit language preference, we should send the application's default. - assert self.user.preferences.model.get_value(self.user, LANGUAGE_KEY) is None - email_marketing_user_field_changed( - None, self.user, table='user_api_userpreference', setting=LANGUAGE_KEY, new_value=None - ) - assert mock_update_user.call_args[0][0]['ui_lang'] == settings.LANGUAGE_CODE - - # If the user has set an explicit language preference, we should send it. - self.user.preferences.create(key=LANGUAGE_KEY, value='fr') - email_marketing_user_field_changed( - None, self.user, table='user_api_userpreference', setting=LANGUAGE_KEY, new_value='fr' - ) - assert mock_update_user.call_args[0][0]['ui_lang'] == 'fr' - - @patch('lms.djangoapps.email_marketing.tasks.update_user_email.delay') - def test_modify_email(self, mock_update_user): - """ - Test that change to email calls update_user_email - """ - email_marketing_user_field_changed(None, self.user, table='auth_user', setting='email', old_value='new@a.com') - mock_update_user.assert_called_with(self.user.email, 'new@a.com') - - # make sure nothing called if disabled - mock_update_user.reset_mock() - update_email_marketing_config(enabled=False) - email_marketing_user_field_changed(None, self.user, table='auth_user', setting='email', old_value='new@a.com') - assert not mock_update_user.called - - -class MockSailthruResponse: - """ - Mock object for SailthruResponse - """ - - def __init__(self, json_response, error=None, code=1): - self.json = json_response - self.error = error - self.code = code - - def is_ok(self): - """ - Return true of no error - """ - return self.error is None - - def get_error(self): - """ - Get error description - """ - return MockSailthruError(self.error, self.code) - - -class MockSailthruError: - """ - Mock object for Sailthru Error - """ - - def __init__(self, error, code=1): - self.error = error - self.code = code - - def get_message(self): - """ - Get error description - """ - return self.error - - def get_error_code(self): - """ - Get error code - """ - return self.code - - -class SailthruTests(TestCase): - """ - Tests for the Sailthru tasks class. - """ - - def setUp(self): - super().setUp() - self.user = UserFactory() - self.course_id = CourseKey.from_string('edX/toy/2012_Fall') - self.course_url = 'http://lms.testserver.fake/courses/edX/toy/2012_Fall/info' - self.course_id2 = 'edX/toy/2016_Fall' - self.course_url2 = 'http://lms.testserver.fake/courses/edX/toy/2016_Fall/info' - - @patch('sailthru.sailthru_client.SailthruClient.purchase') - @patch('sailthru.sailthru_client.SailthruClient.api_get') - @patch('sailthru.sailthru_client.SailthruClient.api_post') - @patch('edx_toggles.toggles.LegacyWaffleSwitchNamespace.is_enabled') - def test_update_course_enrollment_whitelabel( - self, - switch, - mock_sailthru_api_post, - mock_sailthru_api_get, - mock_sailthru_purchase - ): - """test user record not sent to sailthru when enrolled in a course at white label site""" - switch.return_value = True - white_label_site = Site.objects.create(domain='testwhitelabel.com', name='White Label') - site_dict = {'id': white_label_site.id, 'domain': white_label_site.domain, 'name': white_label_site.name} - with patch('lms.djangoapps.email_marketing.signals._get_current_site') as mock_site_info: - mock_site_info.return_value = site_dict - update_sailthru(None, self.user, 'audit', str(self.course_id)) - assert not mock_sailthru_purchase.called - assert not mock_sailthru_api_post.called - assert not mock_sailthru_api_get.called - - @patch('sailthru.sailthru_client.SailthruClient.purchase') - def test_switch_is_disabled(self, mock_sailthru_purchase): - """Make sure sailthru purchase is not called when waffle switch is disabled""" - update_sailthru(None, self.user, 'verified', self.course_id) - assert not mock_sailthru_purchase.called - - @patch('edx_toggles.toggles.LegacyWaffleSwitchNamespace.is_enabled') - @patch('sailthru.sailthru_client.SailthruClient.purchase') - def test_purchase_is_not_invoked(self, mock_sailthru_purchase, switch): - """Make sure purchase is not called in the following condition: - i: waffle switch is True and mode is verified - """ - switch.return_value = True - update_sailthru(None, self.user, 'verified', self.course_id) - assert not mock_sailthru_purchase.called - - @patch('edx_toggles.toggles.LegacyWaffleSwitchNamespace.is_enabled') - @patch('sailthru.sailthru_client.SailthruClient.purchase') - def test_encoding_is_working_for_email_contains_unicode(self, mock_sailthru_purchase, switch): - """Make sure encoding is working for emails contains unicode characters - while sending it to sail through. - """ - switch.return_value = True - self.user.email = 'tèst@edx.org' - update_sailthru(None, self.user, 'audit', str(self.course_id)) - assert mock_sailthru_purchase.called diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py index 883aef9e61..5c7a214fb3 100644 --- a/lms/djangoapps/verify_student/tests/test_views.py +++ b/lms/djangoapps/verify_student/tests/test_views.py @@ -1621,9 +1621,8 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification mock.Mock(side_effect=mocked_has_valid_signature) ) @patch('lms.djangoapps.verify_student.views.log.error') - @patch('sailthru.sailthru_client.SailthruClient.send') @patch('lms.djangoapps.verify_student.views.segment.track') - def test_passed_status_template(self, mock_segment_track, _mock_sailthru_send, _mock_log_error): + def test_passed_status_template(self, mock_segment_track, _mock_log_error): """ Test for verification passed. """ @@ -1694,9 +1693,8 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification mock.Mock(side_effect=mocked_has_valid_signature) ) @patch('lms.djangoapps.verify_student.views.log.error') - @patch('sailthru.sailthru_client.SailthruClient.send') @patch('lms.djangoapps.verify_student.views.segment.track') - def test_first_time_verification(self, mock_segment_track, mock_sailthru_send, mock_log_error): # pylint: disable=unused-argument + def test_first_time_verification(self, mock_segment_track, _mock_log_error): """ Test for verification passed if the learner does not have any previous verification """ @@ -1736,9 +1734,8 @@ class TestPhotoVerificationResultsCallback(ModuleStoreTestCase, TestVerification mock.Mock(side_effect=mocked_has_valid_signature) ) @patch('lms.djangoapps.verify_student.views.log.error') - @patch('sailthru.sailthru_client.SailthruClient.send') @patch('lms.djangoapps.verify_student.views.segment.track') - def test_failed_status_template(self, mock_segment_track, _mock_sailthru_send, _mock_log_error): + def test_failed_status_template(self, mock_segment_track, _mock_log_error): """ Test for failed verification. """ diff --git a/lms/envs/production.py b/lms/envs/production.py index 449c04cca9..2723ea9980 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -1007,14 +1007,6 @@ EXPLICIT_QUEUES = { 'queue': GRADES_DOWNLOAD_ROUTING_KEY}, 'lms.djangoapps.instructor_task.tasks.generate_certificates': { 'queue': GRADES_DOWNLOAD_ROUTING_KEY}, - 'lms.djangoapps.email_marketing.tasks.get_email_cookies_via_sailthru': { - 'queue': ACE_ROUTING_KEY}, - 'lms.djangoapps.email_marketing.tasks.update_user': { - 'queue': ACE_ROUTING_KEY}, - 'lms.djangoapps.email_marketing.tasks.update_user_email': { - 'queue': ACE_ROUTING_KEY}, - 'lms.djangoapps.email_marketing.tasks.update_course_enrollment': { - 'queue': ACE_ROUTING_KEY}, 'lms.djangoapps.verify_student.tasks.send_verification_status_email': { 'queue': ACE_ROUTING_KEY}, 'lms.djangoapps.verify_student.tasks.send_ace_message': { diff --git a/openedx/core/djangoapps/user_authn/apps.py b/openedx/core/djangoapps/user_authn/apps.py index 465e7f7820..a65855b22a 100644 --- a/openedx/core/djangoapps/user_authn/apps.py +++ b/openedx/core/djangoapps/user_authn/apps.py @@ -3,7 +3,7 @@ User Authentication Configuration """ from django.apps import AppConfig -from edx_django_utils.plugins import PluginURLs +from edx_django_utils.plugins import PluginSignals, PluginURLs from openedx.core.djangoapps.plugins.constants import ProjectType @@ -22,4 +22,14 @@ class UserAuthnConfig(AppConfig): PluginURLs.RELATIVE_PATH: 'urls', }, }, + PluginSignals.CONFIG: { + ProjectType.LMS: { + PluginSignals.RECEIVERS: [ + { + PluginSignals.RECEIVER_FUNC_NAME: 'user_fields_changed', + PluginSignals.SIGNAL_PATH: 'common.djangoapps.util.model_utils.USER_FIELDS_CHANGED', + }, + ], + }, + }, } diff --git a/openedx/core/djangoapps/user_authn/signals.py b/openedx/core/djangoapps/user_authn/signals.py new file mode 100644 index 0000000000..a5c3517fc0 --- /dev/null +++ b/openedx/core/djangoapps/user_authn/signals.py @@ -0,0 +1,43 @@ +""" +Signals for user_authn +""" + +from typing import Any, Dict, Optional, Tuple + +from common.djangoapps.student.models import UserProfile +from common.djangoapps.track import segment + + +def user_fields_changed( + user=None, + table=None, + changed_fields: Optional[Dict[str, Tuple[Any, Any]]] = None, + **_kwargs, +): + """ + Update a collection of user profile fields in segment when they change in the database + + Args: + user: The user object for the user being changed + table: The name of the table being updated + changed_fields: A mapping from changed field name to old and new values. + """ + + fields = {field: new_value for (field, (old_value, new_value)) in changed_fields.items()} + # This mirrors the logic in ./views/register.py:_track_user_registration + if table == 'auth_userprofile': + if 'gender' in fields and fields['gender']: + fields['gender'] = dict(UserProfile.GENDER_CHOICES)[fields['gender']] + if 'country' in fields: + fields['country'] = str(fields['country']) + if 'level_of_education' in fields and fields['level_of_education']: + fields['education'] = dict(UserProfile.LEVEL_OF_EDUCATION_CHOICES)[fields['level_of_education']] + if 'year_of_birth' in fields: + fields['yearOfBirth'] = fields.pop('year_of_birth') + if 'mailing_address' in fields: + fields['address'] = fields.pop('mailing_address') + + segment.identify( + user.id, + fields + ) diff --git a/openedx/core/djangoapps/user_authn/tests/test_signals.py b/openedx/core/djangoapps/user_authn/tests/test_signals.py new file mode 100644 index 0000000000..e87ffc5584 --- /dev/null +++ b/openedx/core/djangoapps/user_authn/tests/test_signals.py @@ -0,0 +1,30 @@ +"""Tests for user_authn signals""" + +from unittest.mock import patch +from django.test import TestCase + +from common.djangoapps.student.tests.factories import UserFactory, UserProfileFactory +from openedx.core.djangolib.testing.utils import skip_unless_lms + + +@skip_unless_lms +class UserAuthnSignalTests(TestCase): + """Tests for signal handlers""" + + def test_identify_call_on_user_change(self): + user = UserFactory() + + with patch('openedx.core.djangoapps.user_authn.signals.segment') as mock_segment: + user.email = 'user@example.com' + user.save() + assert mock_segment.identify.call_count == 1 + assert mock_segment.identify.call_args[0] == (user.id, {'email': 'user@example.com'}) + + def test_identify_call_on_profile_change(self): + profile = UserProfileFactory(user=UserFactory()) + + with patch('openedx.core.djangoapps.user_authn.signals.segment') as mock_segment: + profile.gender = 'f' + profile.save() + assert mock_segment.identify.call_count == 1 + assert mock_segment.identify.call_args[0] == (profile.user_id, {'gender': 'Female'}) diff --git a/openedx/features/enterprise_support/signals.py b/openedx/features/enterprise_support/signals.py index 4a2550c94d..316df2702b 100644 --- a/openedx/features/enterprise_support/signals.py +++ b/openedx/features/enterprise_support/signals.py @@ -16,7 +16,6 @@ from integrated_channels.integrated_channel.tasks import ( ) from slumber.exceptions import HttpClientError -from lms.djangoapps.email_marketing.tasks import update_user from openedx.core.djangoapps.commerce.utils import ecommerce_api_client from openedx.core.djangoapps.signals.signals import COURSE_GRADE_NOW_PASSED, COURSE_ASSESSMENT_GRADE_CHANGED from openedx.features.enterprise_support.api import enterprise_enabled @@ -27,23 +26,6 @@ from common.djangoapps.student.signals import UNENROLL_DONE log = logging.getLogger(__name__) -@receiver(post_save, sender=EnterpriseCustomerUser) -def update_email_marketing_user_with_enterprise_vars(sender, instance, **kwargs): # pylint: disable=unused-argument, invalid-name - """ - Update the SailThru user with enterprise-related vars. - """ - user = User.objects.get(id=instance.user_id) - - # perform update asynchronously - update_user.delay( - sailthru_vars={ - 'is_enterprise_learner': True, - 'enterprise_name': instance.enterprise_customer.name, - }, - email=user.email - ) - - @receiver(post_save, sender=EnterpriseCourseEnrollment) def update_dsc_cache_on_course_enrollment(sender, instance, **kwargs): # pylint: disable=unused-argument """ diff --git a/openedx/features/enterprise_support/tests/test_signals.py b/openedx/features/enterprise_support/tests/test_signals.py index c0809416fc..9143e0e355 100644 --- a/openedx/features/enterprise_support/tests/test_signals.py +++ b/openedx/features/enterprise_support/tests/test_signals.py @@ -77,20 +77,6 @@ class EnterpriseSupportSignals(SharedModuleStoreTestCase): enterprise_customer_user=enterprise_customer_user, ) - @patch('openedx.features.enterprise_support.signals.update_user.delay') - def test_register_user(self, mock_update_user): - """ - make sure marketing enterprise user call invokes update_user - """ - self._create_enterprise_enrollment(self.user.id, self.course_id) - mock_update_user.assert_called_with( - sailthru_vars={ - 'is_enterprise_learner': True, - 'enterprise_name': self.enterprise_customer.name, - }, - email=self.user.email - ) - def test_signal_update_dsc_cache_on_course_enrollment(self): """ make sure update_dsc_cache_on_course_enrollment signal clears cache when Enterprise Course Enrollment diff --git a/requirements/edx/base.in b/requirements/edx/base.in index fa8ca7fb4a..727deabf76 100644 --- a/requirements/edx/base.in +++ b/requirements/edx/base.in @@ -146,7 +146,6 @@ requests-oauthlib # Simplifies use of OAuth via the requests l random2 rules # Django extension for rules-based authorization checks simplejson -sailthru-client # For Sailthru integration Shapely # Geometry library, used for image click regions in capa six # Utilities for supporting Python 2 & 3 in the same codebase social-auth-app-django