Files
edx-platform/lms/djangoapps/email_marketing/signals.py

321 lines
12 KiB
Python

"""
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)