Merge pull request #27502 from edx/mikix/drop-email-marketing
feat!: remove all email_marketing djangoapp code
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -4,6 +4,5 @@ from common.djangoapps.student.signals.signals import (
|
||||
ENROLL_STATUS_CHANGE,
|
||||
ENROLLMENT_TRACK_UPDATED,
|
||||
REFUND_ORDER,
|
||||
SAILTHRU_AUDIT_PURCHASE,
|
||||
UNENROLL_DONE
|
||||
)
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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`` |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
43
openedx/core/djangoapps/user_authn/signals.py
Normal file
43
openedx/core/djangoapps/user_authn/signals.py
Normal file
@@ -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
|
||||
)
|
||||
30
openedx/core/djangoapps/user_authn/tests/test_signals.py
Normal file
30
openedx/core/djangoapps/user_authn/tests/test_signals.py
Normal file
@@ -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'})
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user