Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions common/djangoapps/user_api/api/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
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. """
Expand Down Expand Up @@ -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))
84 changes: 83 additions & 1 deletion common/djangoapps/user_api/tests/test_profile_api.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -94,6 +98,84 @@ def test_update_and_retrieve_preference_info(self):
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be necessary, since test settings inherit from common. Maybe this setting needs to be added to cms/envs/common.py as well, since the app is in common/djangoapps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't necessary, I had just added it as a precaution in case someone changed the settings locally in their common.py, this test would still pass.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test case for updating a preference that has already been set.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a test case for when the user's year of birth isn't provided.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the new test cases. Caught some problems with my code, too, so thanks!


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)
Expand Down
4 changes: 4 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,10 @@
# 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 ##########################################

Expand Down