diff --git a/common/djangoapps/user_api/api/profile.py b/common/djangoapps/user_api/api/profile.py index 35356b6256..4b5ae6a945 100644 --- a/common/djangoapps/user_api/api/profile.py +++ b/common/djangoapps/user_api/api/profile.py @@ -5,11 +5,16 @@ but does NOT include basic account information such as username, password, and email address. """ +import datetime +from django.conf import settings +from django.db import IntegrityError +import logging +from pytz import UTC - -from user_api.models import User, UserProfile, UserPreference +from user_api.models import User, UserProfile, UserPreference, UserOrgTag from user_api.helpers import intercept_errors +log = logging.getLogger(__name__) class ProfileRequestError(Exception): """ The request to the API was not valid. """ @@ -155,3 +160,45 @@ def update_preferences(username, **kwargs): else: for key, value in kwargs.iteritems(): UserPreference.set_preference(user, key, value) + + +@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError]) +def update_email_opt_in(username, org, optin): + """Updates a user's preference for receiving org-wide emails. + + Sets a User Org Tag defining the choice to opt in or opt out of organization-wide + emails. + + Args: + username (str): The user to set a preference for. + org (str): The org is used to determine the organization this setting is related to. + optin (boolean): True if the user is choosing to receive emails for this organization. If the user is not + the correct age to receive emails, email-optin is set to False regardless. + + Returns: + None + + Raises: + ProfileUserNotFound: Raised when the username specified is not associated with a user. + + """ + try: + user = User.objects.get(username=username) + except User.DoesNotExist: + raise ProfileUserNotFound + + profile = UserProfile.objects.get(user=user) + of_age = ( + profile.year_of_birth is None or # If year of birth is not set, we assume user is of age. + datetime.datetime.now(UTC).year - profile.year_of_birth >= # pylint: disable=maybe-no-member + getattr(settings, 'EMAIL_OPTIN_MINIMUM_AGE', 13) + ) + + try: + preference, _ = UserOrgTag.objects.get_or_create( + user=user, org=org, key='email-optin' + ) + preference.value = str(optin and of_age) + preference.save() + except IntegrityError as err: + log.warn(u"Could not update organization wide preference due to IntegrityError: {}".format(err.message)) diff --git a/common/djangoapps/user_api/tests/test_profile_api.py b/common/djangoapps/user_api/tests/test_profile_api.py index 5f78c0a832..65f62cc061 100644 --- a/common/djangoapps/user_api/tests/test_profile_api.py +++ b/common/djangoapps/user_api/tests/test_profile_api.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- """ Tests for the profile API. """ +from django.contrib.auth.models import User from django.test import TestCase import ddt +from django.test.utils import override_settings from nose.tools import raises from dateutil.parser import parse as parse_datetime +from xmodule.modulestore.tests.factories import CourseFactory +import datetime from user_api.api import account as account_api from user_api.api import profile as profile_api -from user_api.models import UserProfile +from user_api.models import UserProfile, UserOrgTag @ddt.ddt @@ -94,6 +98,84 @@ class ProfileApiTest(TestCase): preferences = profile_api.preference_info(self.USERNAME) self.assertEqual(preferences['preference_key'], 'preference_value') + @ddt.data( + # Check that a 27 year old can opt-in + (27, True, u"True"), + + # Check that a 32-year old can opt-out + (32, False, u"False"), + + # Check that someone 13 years old can opt-in + (13, True, u"True"), + + # Check that someone 12 years old cannot opt-in + (12, True, u"False") + ) + @ddt.unpack + @override_settings(EMAIL_OPTIN_MINIMUM_AGE=13) + def test_update_email_optin(self, age, option, expected_result): + # Create the course and account. + course = CourseFactory.create() + account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL) + + # Set year of birth + user = User.objects.get(username=self.USERNAME) + profile = UserProfile.objects.get(user=user) + year_of_birth = datetime.datetime.now().year - age # pylint: disable=maybe-no-member + profile.year_of_birth = year_of_birth + profile.save() + + profile_api.update_email_opt_in(self.USERNAME, course.id.org, option) + result_obj = UserOrgTag.objects.get(user=user, org=course.id.org, key='email-optin') + self.assertEqual(result_obj.value, expected_result) + + def test_update_email_optin_no_age_set(self): + # Test that the API still works if no age is specified. + # Create the course and account. + course = CourseFactory.create() + account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL) + + user = User.objects.get(username=self.USERNAME) + + profile_api.update_email_opt_in(self.USERNAME, course.id.org, True) + result_obj = UserOrgTag.objects.get(user=user, org=course.id.org, key='email-optin') + self.assertEqual(result_obj.value, u"True") + + @ddt.data( + # Check that a 27 year old can opt-in, then out. + (27, True, False, u"False"), + + # Check that a 32-year old can opt-out, then in. + (32, False, True, u"True"), + + # Check that someone 13 years old can opt-in, then out. + (13, True, False, u"False"), + + # Check that someone 12 years old cannot opt-in, then explicitly out. + (12, True, False, u"False") + ) + @ddt.unpack + @override_settings(EMAIL_OPTIN_MINIMUM_AGE=13) + def test_change_email_optin(self, age, option, second_option, expected_result): + # Create the course and account. + course = CourseFactory.create() + account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL) + + # Set year of birth + user = User.objects.get(username=self.USERNAME) + profile = UserProfile.objects.get(user=user) + year_of_birth = datetime.datetime.now().year - age # pylint: disable=maybe-no-member + profile.year_of_birth = year_of_birth + profile.save() + + profile_api.update_email_opt_in(self.USERNAME, course.id.org, option) + profile_api.update_email_opt_in(self.USERNAME, course.id.org, second_option) + + result_obj = UserOrgTag.objects.get(user=user, org=course.id.org, key='email-optin') + self.assertEqual(result_obj.value, expected_result) + + + @raises(profile_api.ProfileUserNotFound) def test_retrieve_and_update_preference_info_no_user(self): preferences = profile_api.preference_info(self.USERNAME) diff --git a/lms/envs/common.py b/lms/envs/common.py index 7d42ddbdf0..8e167c0074 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1365,6 +1365,10 @@ BULK_EMAIL_LOG_SENT_EMAILS = False # parallel, and what the SES rate is. BULK_EMAIL_RETRY_DELAY_BETWEEN_SENDS = 0.02 +############################# Email Opt In #################################### + +# Minimum age for organization-wide email opt in +EMAIL_OPTIN_MINIMUM_AGE = 13 ############################## Video ##########################################