Commit 6976a33a by Andy Armstrong

Add the preferences endpoint to the User API

TNL-1493

See https://openedx.atlassian.net/wiki/display/TNL/User+API for details
parent 63341287
......@@ -6,7 +6,7 @@ consist primarily of authentication, request validation, and serialization.
from ipware.ip import get_ip
from django.utils.decorators import method_decorator
from opaque_keys import InvalidKeyError
from openedx.core.djangoapps.user_api import api as user_api
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeaderPermissionIsAuthenticated
from rest_framework import status
from rest_framework.response import Response
......@@ -349,7 +349,7 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
email_opt_in = request.DATA.get('email_opt_in', None)
if email_opt_in is not None:
org = course_id.org
user_api.profile.update_email_opt_in(request.user, org, email_opt_in)
update_email_opt_in(request.user, org, email_opt_in)
return Response(response)
except CourseModeNotFoundError as error:
return Response(
......
......@@ -2,7 +2,7 @@
Middleware for Language Preferences
"""
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from lang_pref import LANGUAGE_KEY
......@@ -20,6 +20,6 @@ class LanguagePreferenceMiddleware(object):
no language set on the session (i.e. from dark language overrides), use the user's preference.
"""
if request.user.is_authenticated() and 'django_language' not in request.session:
user_pref = UserPreference.get_preference(request.user, LANGUAGE_KEY)
user_pref = get_user_preference(request.user, LANGUAGE_KEY)
if user_pref:
request.session['django_language'] = user_pref
......@@ -3,7 +3,7 @@ from django.test.client import RequestFactory
from django.contrib.sessions.middleware import SessionMiddleware
from lang_pref.middleware import LanguagePreferenceMiddleware
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from lang_pref import LANGUAGE_KEY
from student.tests.factories import UserFactory
......@@ -28,7 +28,7 @@ class TestUserPreferenceMiddleware(TestCase):
def test_language_in_user_prefs(self):
# language set in the user preferences and not the session
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
self.middleware.process_request(self.request)
self.assertEquals(self.request.session['django_language'], 'eo')
......@@ -36,7 +36,7 @@ class TestUserPreferenceMiddleware(TestCase):
# language set in both the user preferences and session,
# session should get precedence
self.request.session['django_language'] = 'en'
UserPreference.set_preference(self.user, LANGUAGE_KEY, 'eo')
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
self.middleware.process_request(self.request)
self.assertEquals(self.request.session['django_language'], 'en')
......@@ -4,7 +4,7 @@ Tests for the language setting view
from django.core.urlresolvers import reverse
from django.test import TestCase
from student.tests.factories import UserFactory
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from lang_pref import LANGUAGE_KEY
......@@ -20,7 +20,7 @@ class TestLanguageSetting(TestCase):
response = self.client.post(reverse('lang_pref_set_language'), {'language': lang})
self.assertEquals(response.status_code, 200)
user_pref = UserPreference.get_preference(user, LANGUAGE_KEY)
user_pref = get_user_preference(user, LANGUAGE_KEY)
self.assertEqual(user_pref, lang)
def test_set_preference_missing_lang(self):
......@@ -31,4 +31,4 @@ class TestLanguageSetting(TestCase):
self.assertEquals(response.status_code, 400)
self.assertIsNone(UserPreference.get_preference(user, LANGUAGE_KEY))
self.assertIsNone(get_user_preference(user, LANGUAGE_KEY))
......@@ -4,7 +4,7 @@ Views for accessing language preferences
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, HttpResponseBadRequest
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from lang_pref import LANGUAGE_KEY
......@@ -13,11 +13,10 @@ def set_language(request):
"""
This view is called when the user would like to set a language preference
"""
user = request.user
lang_pref = request.POST.get('language', None)
if lang_pref:
UserPreference.set_preference(user, LANGUAGE_KEY, lang_pref)
set_user_preference(request.user, LANGUAGE_KEY, lang_pref)
return HttpResponse('{"success": true}')
return HttpResponseBadRequest('no language provided')
......@@ -40,7 +40,7 @@ class UserProfileFactory(DjangoModelFactory):
level_of_education = None
gender = u'm'
mailing_address = None
goals = u'World domination'
goals = u'Learn a lot'
class CourseModeFactory(DjangoModelFactory):
......
"Tests for account creation"
"""Tests for account creation"""
import json
import ddt
......@@ -14,7 +14,7 @@ from django.test.utils import override_settings
import mock
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from lang_pref import LANGUAGE_KEY
from notification_prefs import NOTIFICATION_PREF_KEY
......@@ -42,7 +42,7 @@ TEST_CS_URL = 'https://comments.service.test:123/'
}
)
class TestCreateAccount(TestCase):
"Tests for account creation"
"""Tests for account creation"""
def setUp(self):
self.username = "test_user"
......@@ -63,14 +63,14 @@ class TestCreateAccount(TestCase):
response = self.client.post(self.url, self.params)
self.assertEqual(response.status_code, 200)
user = User.objects.get(username=self.username)
self.assertEqual(UserPreference.get_preference(user, LANGUAGE_KEY), lang)
self.assertEqual(get_user_preference(user, LANGUAGE_KEY), lang)
@ddt.data("en", "eo")
def test_header_lang_pref_saved(self, lang):
response = self.client.post(self.url, self.params, HTTP_ACCEPT_LANGUAGE=lang)
user = User.objects.get(username=self.username)
self.assertEqual(response.status_code, 200)
self.assertEqual(UserPreference.get_preference(user, LANGUAGE_KEY), lang)
self.assertEqual(get_user_preference(user, LANGUAGE_KEY), lang)
def create_account_and_fetch_profile(self):
"""
......@@ -225,7 +225,7 @@ class TestCreateAccount(TestCase):
response = self.client.post(self.url, self.params)
self.assertEqual(response.status_code, 200)
user = User.objects.get(username=self.username)
preference = UserPreference.get_preference(user, NOTIFICATION_PREF_KEY)
preference = get_user_preference(user, NOTIFICATION_PREF_KEY)
if digest_enabled:
self.assertIsNotNone(preference)
else:
......
......@@ -103,7 +103,7 @@ class EnrollmentTest(UrlResetMixin, ModuleStoreTestCase):
self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
@patch.dict(settings.FEATURES, {'ENABLE_MKTG_EMAIL_OPT_IN': True})
@patch('openedx.core.djangoapps.user_api.api.profile.update_email_opt_in')
@patch('openedx.core.djangoapps.user_api.preferences.api.update_email_opt_in')
@ddt.data(
([], 'true'),
([], 'false'),
......
......@@ -85,7 +85,6 @@ from external_auth.login_and_register import (
from bulk_email.models import Optout, CourseAuthorization
import shoppingcart
from lang_pref import LANGUAGE_KEY
from notification_prefs.views import enable_notifications
import track.views
......@@ -118,6 +117,12 @@ from embargo import api as embargo_api
import analytics
from eventtracking import tracker
# Note that this lives in LMS, so this dependency should be refactored.
from notification_prefs.views import enable_notifications
# Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
......@@ -632,20 +637,17 @@ def dashboard(request):
# Re-alphabetize language options
language_options.sort()
# TODO: remove circular dependency on openedx from common
from openedx.core.djangoapps.user_api.models import UserPreference
# try to get the prefered language for the user
cur_pref_lang_code = UserPreference.get_preference(request.user, LANGUAGE_KEY)
# try to get the preferred language for the user
preferred_language_code = preferences_api.get_user_preference(request.user, LANGUAGE_KEY)
# try and get the current language of the user
cur_lang_code = get_language()
if cur_pref_lang_code and cur_pref_lang_code in settings.LANGUAGE_DICT:
current_language_code = get_language()
if preferred_language_code and preferred_language_code in settings.LANGUAGE_DICT:
# if the user has a preference, get the name from the code
current_language = settings.LANGUAGE_DICT[cur_pref_lang_code]
elif cur_lang_code in settings.LANGUAGE_DICT:
current_language = settings.LANGUAGE_DICT[preferred_language_code]
elif current_language_code in settings.LANGUAGE_DICT:
# if the user's browser is showing a particular language,
# use that as the current language
current_language = settings.LANGUAGE_DICT[cur_lang_code]
current_language = settings.LANGUAGE_DICT[current_language_code]
else:
# otherwise, use the default language
current_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
......@@ -680,7 +682,7 @@ def dashboard(request):
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
'language_options': language_options,
'current_language': current_language,
'current_language_code': cur_lang_code,
'current_language_code': current_language_code,
'user': user,
'duplicate_provider': None,
'logout_url': reverse(logout_user),
......@@ -800,13 +802,10 @@ def try_change_enrollment(request):
def _update_email_opt_in(request, org):
"""Helper function used to hit the profile API if email opt-in is enabled."""
# TODO: remove circular dependency on openedx from common
from openedx.core.djangoapps.user_api.api import profile as profile_api
email_opt_in = request.POST.get('email_opt_in')
if email_opt_in is not None:
email_opt_in_boolean = email_opt_in == 'true'
profile_api.update_email_opt_in(request.user, org, email_opt_in_boolean)
preferences_api.update_email_opt_in(request.user, org, email_opt_in_boolean)
@require_POST
......@@ -1391,10 +1390,7 @@ def _do_create_account(form):
log.exception("UserProfile creation failed for user {id}.".format(id=user.id))
raise
# TODO: remove circular dependency on openedx from common
from openedx.core.djangoapps.user_api.models import UserPreference
UserPreference.set_preference(user, LANGUAGE_KEY, get_language())
preferences_api.set_user_preference(user, LANGUAGE_KEY, get_language())
return (user, profile, registration)
......
......@@ -89,6 +89,9 @@ from logging import getLogger
from . import provider
# Note that this lives in openedx, so this dependency should be refactored.
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
# These are the query string params you can pass
# to the URL that starts the authentication process.
......@@ -669,10 +672,8 @@ def change_enrollment(strategy, user=None, is_dashboard=False, *args, **kwargs):
# If the email opt in parameter is found, set the preference.
email_opt_in = strategy.session_get(AUTH_EMAIL_OPT_IN_KEY)
if email_opt_in:
# TODO: remove circular dependency on openedx from common
from openedx.core.djangoapps.user_api.api import profile
opt_in = email_opt_in.lower() == 'true'
profile.update_email_opt_in(user, course_id.org, opt_in)
update_email_opt_in(user, course_id.org, opt_in)
# Check whether we're blocked from enrolling by a
# country access rule.
......
......@@ -79,7 +79,9 @@ def get_user_email_language(user):
Return the language most appropriate for writing emails to user. Returns
None if the preference has not been set, or if the user does not exist.
"""
return UserPreference.get_preference(user, LANGUAGE_KEY)
# Calling UserPreference directly instead of get_user_preference because the user requesting the
# information is not "user" and also may not have is_staff access.
return UserPreference.get_value(user, LANGUAGE_KEY)
def enroll_email(course_id, student_email, auto_enroll=False, email_students=False, email_params=None, language=None):
......
......@@ -10,7 +10,7 @@ from courseware.tests.factories import InstructorFactory
from lang_pref import LANGUAGE_KEY
from student.models import CourseEnrollment
from student.tests.factories import UserFactory
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
......@@ -29,11 +29,11 @@ class TestInstructorAPIEnrollmentEmailLocalization(ModuleStoreTestCase):
# French.
self.course = CourseFactory.create()
self.instructor = InstructorFactory(course_key=self.course.id)
UserPreference.set_preference(self.instructor, LANGUAGE_KEY, 'zh-cn')
set_user_preference(self.instructor, LANGUAGE_KEY, 'zh-cn')
self.client.login(username=self.instructor.username, password='test')
self.student = UserFactory.create()
UserPreference.set_preference(self.student, LANGUAGE_KEY, 'fr')
set_user_preference(self.student, LANGUAGE_KEY, 'fr')
def update_enrollement(self, action, student_email):
"""
......
......@@ -79,7 +79,7 @@ import instructor_analytics.basic
import instructor_analytics.distributions
import instructor_analytics.csvs
import csv
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
from instructor.views import INVOICE_KEY
from submissions import api as sub_api # installed from the edx-submissions repository
......@@ -1238,7 +1238,7 @@ def generate_registration_codes(request, course_id):
invoice_copy = True
sale_price = unit_price * course_code_number
UserPreference.set_preference(request.user, INVOICE_KEY, invoice_copy)
set_user_preference(request.user, INVOICE_KEY, invoice_copy)
sale_invoice = Invoice.objects.create(
total_amount=sale_price,
company_name=company_name,
......@@ -2187,8 +2187,9 @@ def get_user_invoice_preference(request, course_id): # pylint: disable=unused-a
Gets invoice copy user's preferences.
"""
invoice_copy_preference = True
if UserPreference.get_preference(request.user, INVOICE_KEY) is not None:
invoice_copy_preference = UserPreference.get_preference(request.user, INVOICE_KEY) == 'True'
invoice_preference_value = get_user_preference(request.user, INVOICE_KEY)
if invoice_preference_value is not None:
invoice_copy_preference = invoice_preference_value == 'True'
return JsonResponse({
'invoice_copy': invoice_copy_preference
......
......@@ -18,7 +18,7 @@ from xmodule.partitions.partitions import Group, UserPartition
from openedx.core.djangoapps.course_groups.models import CourseUserGroupPartitionGroup
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory
import openedx.core.djangoapps.user_api.api.course_tag as course_tag_api
import openedx.core.djangoapps.user_api.course_tag.api as course_tag_api
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from instructor_task.models import ReportStore
from instructor_task.tasks_helper import cohort_students_and_upload, upload_grades_csv, upload_students_csv
......
......@@ -8,7 +8,7 @@ import xblock.reference.plugins
from django.core.urlresolvers import reverse
from django.conf import settings
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
from openedx.core.djangoapps.user_api.api import course_tag as user_course_tag_api
from openedx.core.djangoapps.user_api.course_tag import api as user_course_tag_api
from xmodule.modulestore.django import modulestore
from xmodule.services import SettingsService
from xmodule.library_tools import LibraryToolsService
......
......@@ -5,7 +5,7 @@ Views for users sharing preferences
from rest_framework import generics, status
from rest_framework.response import Response
from openedx.core.djangoapps.user_api.api.profile import preference_info, update_preferences
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences, set_user_preference
from ...utils import mobile_view
from . import serializers
......@@ -42,11 +42,11 @@ class UserSharing(generics.ListCreateAPIView):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
value = serializer.object['share_with_facebook_friends']
update_preferences(request.user.username, share_with_facebook_friends=value)
set_user_preference(request.user, "share_with_facebook_friends", value)
return self.get(request, *args, **kwargs)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get(self, request, *args, **kwargs):
preferences = preference_info(request.user.username)
preferences = get_user_preferences(request.user)
response = {'share_with_facebook_friends': preferences.get('share_with_facebook_friends', 'False')}
return Response(response)
......@@ -12,7 +12,7 @@ from social.apps.django_app.default.models import UserSocialAuth
from student.models import CourseEnrollment
from student.views import login_oauth_token
from openedx.core.djangoapps.user_api.api.profile import preference_info, update_preferences
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from courseware.tests.factories import UserFactory
......@@ -132,8 +132,9 @@ class SocialFacebookTestCase(ModuleStoreTestCase, APITestCase):
"""
Sets self.user's share settings to boolean_value
"""
update_preferences(user.username, share_with_facebook_friends=boolean_value)
self.assertEqual(preference_info(user.username)['share_with_facebook_friends'], unicode(boolean_value))
# Note that setting the value to boolean will result in the conversion to the unicode form of the boolean.
set_user_preference(user, 'share_with_facebook_friends', boolean_value)
self.assertEqual(get_user_preference(user, 'share_with_facebook_friends'), unicode(boolean_value))
def _change_enrollment(self, action, course_id=None, email_opt_in=None):
"""
......
......@@ -5,10 +5,12 @@ import json
import urllib2
import facebook
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from rest_framework import status
from rest_framework.response import Response
from social.apps.django_app.default.models import UserSocialAuth
from openedx.core.djangoapps.user_api.api.profile import preference_info
from openedx.core.djangoapps.user_api.models import UserPreference
from student.models import User
# TODO
......@@ -64,5 +66,11 @@ def share_with_facebook_friends(friend):
"""
Return true if the user's share_with_facebook_friends preference is set to true.
"""
share_fb_friends_settings = preference_info(friend['edX_username'])
return share_fb_friends_settings.get('share_with_facebook_friends', None) == 'True'
# Calling UserPreference directly because the requesting user may be different (and not is_staff).
try:
existing_user = User.objects.get(username=friend['edX_username'])
except ObjectDoesNotExist:
return False
return UserPreference.get_value(existing_user, 'share_with_facebook_friends') == 'True'
from django.contrib.auth.models import User
from lettuce import step, world
from notification_prefs import NOTIFICATION_PREF_KEY
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference, get_user_preference
USERNAME = "robot"
......@@ -11,7 +11,7 @@ UNSUB_TOKEN = "av9E-14sAP1bVBRCPbrTHQ=="
@step(u"I have notifications enabled")
def enable_notifications(step_):
user = User.objects.get(username=USERNAME)
UserPreference.objects.create(user=user, key=NOTIFICATION_PREF_KEY, value=UNSUB_TOKEN)
set_user_preference(user, NOTIFICATION_PREF_KEY, UNSUB_TOKEN)
@step(u"I access my unsubscribe url")
......@@ -22,4 +22,4 @@ def access_unsubscribe_url(step_):
@step(u"my notifications should be disabled")
def notifications_should_be_disabled(step_):
user = User.objects.get(username=USERNAME)
assert not UserPreference.objects.filter(user=user, key=NOTIFICATION_PREF_KEY).exists()
assert not get_user_preference(user, NOTIFICATION_PREF_KEY)
......@@ -13,6 +13,7 @@ from django.views.decorators.http import require_GET, require_POST
from edxmako.shortcuts import render_to_response
from notification_prefs import NOTIFICATION_PREF_KEY
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import delete_user_preference
class UsernameDecryptionException(Exception):
......@@ -95,6 +96,8 @@ def enable_notifications(user):
Enable notifications for a user.
Currently only used for daily forum digests.
"""
# Calling UserPreference directly because this method is called from a couple of places,
# and it is not clear that user is always the user initiating the request.
UserPreference.objects.get_or_create(
user=user,
key=NOTIFICATION_PREF_KEY,
......@@ -104,17 +107,6 @@ def enable_notifications(user):
)
def disable_notifications(user):
"""
Disable notifications for a user.
Currently only used for daily forum digests.
"""
UserPreference.objects.filter(
user=user,
key=NOTIFICATION_PREF_KEY
).delete()
@require_POST
def ajax_enable(request):
"""
......@@ -123,7 +115,7 @@ def ajax_enable(request):
This view should be invoked by an AJAX POST call. It returns status 204
(no content) or an error. If notifications were already enabled for this
user, this has no effect. Otherwise, a preference is created with the
unsubscribe token (an ecnryption of the username) as the value.unsernam
unsubscribe token (an encryption of the username) as the value.username
"""
if not request.user.is_authenticated():
raise PermissionDenied
......@@ -144,7 +136,7 @@ def ajax_disable(request):
if not request.user.is_authenticated():
raise PermissionDenied
disable_notifications(request.user)
delete_user_preference(request.user, NOTIFICATION_PREF_KEY)
return HttpResponse(status=204)
......@@ -192,6 +184,8 @@ def set_subscription(request, token, subscribe): # pylint: disable=unused-argum
except User.DoesNotExist:
raise Http404("username")
# Calling UserPreference directly because the fact that the user is passed in the token implies
# that it may not match request.user.
if subscribe:
UserPreference.objects.get_or_create(user=user,
key=NOTIFICATION_PREF_KEY,
......
......@@ -66,10 +66,10 @@ class ProfileHandler(object):
"""
Return the locale for the users based on their preferences.
Does not return a value if the users have not set their locale preferences.
"""
language = UserPreference.get_preference(data['user'], LANGUAGE_KEY)
# Calling UserPreference directly because it is not clear which user made the request.
language = UserPreference.get_value(data['user'], LANGUAGE_KEY)
# If the user has no language specified, return the default one.
if not language:
......
......@@ -9,7 +9,7 @@ from student.models import anonymous_id_for_user
from student.models import UserProfile
from student.roles import CourseStaffRole, CourseInstructorRole
from student.tests.factories import UserFactory, UserProfileFactory
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
# Will also run default tests for IDTokens and UserInfo
......@@ -68,7 +68,7 @@ class IDTokenTest(BaseTestMixin, IDTokenTestCase):
def test_user_with_locale_claim(self):
language = 'en'
UserPreference.set_preference(self.user, LANGUAGE_KEY, language)
set_user_preference(self.user, LANGUAGE_KEY, language)
scopes, claims = self.get_id_token_values('openid profile')
self.assertIn('profile', scopes)
......
......@@ -18,8 +18,8 @@ from django.test.utils import override_settings
from util.testing import UrlResetMixin
from third_party_auth.tests.testutil import simulate_running_pipeline
from embargo.test_utils import restrict_course
from openedx.core.djangoapps.user_api.api import account as account_api
from openedx.core.djangoapps.user_api.api import profile as profile_api
from openedx.core.djangoapps.user_api.accounts.api import activate_account, create_account
from openedx.core.djangoapps.user_api.accounts import EMAIL_MAX_LENGTH
from xmodule.modulestore.tests.django_utils import (
ModuleStoreTestCase, mixed_store_config
)
......@@ -53,7 +53,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
# Long email -- subtract the length of the @domain
# except for one character (so we exceed the max length limit)
u"{user}@example.com".format(
user=(u'e' * (account_api.EMAIL_MAX_LENGTH - 11))
user=(u'e' * (EMAIL_MAX_LENGTH - 11))
)
]
......@@ -63,8 +63,8 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
super(StudentAccountUpdateTest, self).setUp("student_account.urls")
# Create/activate a new account
activation_key = account_api.create_account(self.USERNAME, self.OLD_PASSWORD, self.OLD_EMAIL)
account_api.activate_account(activation_key)
activation_key = create_account(self.USERNAME, self.OLD_PASSWORD, self.OLD_EMAIL)
activate_account(activation_key)
# Login
result = self.client.login(username=self.USERNAME, password=self.OLD_PASSWORD)
......@@ -148,7 +148,7 @@ class StudentAccountUpdateTest(UrlResetMixin, TestCase):
self.client.logout()
# Create a second user, but do not activate it
account_api.create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
create_account(self.ALTERNATE_USERNAME, self.OLD_PASSWORD, self.NEW_EMAIL)
# Send the view the email address tied to the inactive user
response = self._change_password(email=self.NEW_EMAIL)
......@@ -226,8 +226,8 @@ class StudentAccountLoginAndRegistrationTest(UrlResetMixin, ModuleStoreTestCase)
@ddt.data("account_login", "account_register")
def test_login_and_registration_form_already_authenticated(self, url_name):
# Create/activate a new account and log in
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
account_api.activate_account(activation_key)
activation_key = create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
activate_account(activation_key)
result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.assertTrue(result)
......
......@@ -11,15 +11,13 @@ from django.http import (
from django.shortcuts import redirect
from django.http import HttpRequest
from django.core.urlresolvers import reverse, resolve
from django.core.mail import send_mail
from django.utils.translation import ugettext as _
from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from opaque_keys.edx.keys import CourseKey
from opaque_keys import InvalidKeyError
from edxmako.shortcuts import render_to_response, render_to_string
from edxmako.shortcuts import render_to_response
from microsite_configuration import microsite
from embargo import api as embargo_api
import third_party_auth
......@@ -32,8 +30,8 @@ from student.views import (
register_user as old_register_view
)
from openedx.core.djangoapps.user_api.api import account as account_api
from openedx.core.djangoapps.user_api.api import profile as profile_api
from openedx.core.djangoapps.user_api.accounts.api import request_password_change
from openedx.core.djangoapps.user_api.errors import UserNotFound
from util.bad_request_rate_limiter import BadRequestRateLimiter
from student_account.helpers import auth_pipeline_urls
......@@ -136,8 +134,8 @@ def password_change_request_handler(request):
if email:
try:
account_api.request_password_change(email, request.get_host(), request.is_secure())
except account_api.AccountUserNotFound:
request_password_change(email, request.get_host(), request.is_secure())
except UserNotFound:
AUDIT_LOG.info("Invalid password reset attempt")
# Increment the rate limit counter
limiter.tick_bad_request_counter(request)
......
......@@ -29,7 +29,7 @@ from django.core.mail import send_mail
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings, update_account_settings
from openedx.core.djangoapps.user_api.accounts import NAME_MIN_LENGTH
from openedx.core.djangoapps.user_api.api.account import AccountUserNotFound, AccountValidationError
from openedx.core.djangoapps.user_api.errors import UserNotFound, AccountValidationError
from course_modes.models import CourseMode
from student.models import CourseEnrollment
......@@ -734,7 +734,7 @@ def submit_photos_for_verification(request):
if request.POST.get('full_name'):
try:
update_account_settings(request.user, {"name": request.POST.get('full_name')})
except AccountUserNotFound:
except UserNotFound:
return HttpResponseBadRequest(_("No profile found for user"))
except AccountValidationError:
msg = _(
......
......@@ -2,8 +2,21 @@
Account constants
"""
# The minimum acceptable length for the name account field
# The minimum and maximum length for the name ("full name") account field
NAME_MIN_LENGTH = 2
NAME_MAX_LENGTH = 255
# The minimum and maximum length for the username account field
USERNAME_MIN_LENGTH = 2
USERNAME_MAX_LENGTH = 30
# The minimum and maximum length for the email account field
EMAIL_MIN_LENGTH = 3
EMAIL_MAX_LENGTH = 254
# The minimum and maximum length for the password account field
PASSWORD_MIN_LENGTH = 2
PASSWORD_MAX_LENGTH = 75
ACCOUNT_VISIBILITY_PREF_KEY = 'account_privacy'
......
......@@ -10,8 +10,8 @@ class AccountUserSerializer(serializers.HyperlinkedModelSerializer):
"""
class Meta:
model = User
fields = ("username", "email", "date_joined")
read_only_fields = ("username", "email", "date_joined")
fields = ("username", "email", "date_joined", "is_active")
read_only_fields = ("username", "email", "date_joined", "is_active")
class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer):
......
......@@ -7,12 +7,10 @@ https://openedx.atlassian.net/wiki/display/TNL/User+API
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
from rest_framework import permissions
from openedx.core.djangoapps.user_api.api.account import (
AccountUserNotFound, AccountUpdateError, AccountNotAuthorized, AccountValidationError
)
from ..errors import UserNotFound, UserNotAuthorized, AccountUpdateError, AccountValidationError
from openedx.core.lib.api.parsers import MergePatchParser
from .api import get_account_settings, update_account_settings
......@@ -93,9 +91,9 @@ class AccountView(APIView):
If the update could not be completed due to failure at the time of update, this method returns a 400 with
specific errors in the returned JSON.
If the updated is successful, a 204 status is returned with no additional content.
If the update is successful, a 204 status is returned with no additional content.
"""
authentication_classes = (OAuth2Authentication, SessionAuthentication)
authentication_classes = (OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser)
permission_classes = (permissions.IsAuthenticated,)
parser_classes = (MergePatchParser,)
......@@ -105,7 +103,7 @@ class AccountView(APIView):
"""
try:
account_settings = get_account_settings(request.user, username, view=request.QUERY_PARAMS.get('view'))
except AccountUserNotFound:
except UserNotFound:
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(account_settings)
......@@ -120,7 +118,7 @@ class AccountView(APIView):
"""
try:
update_account_settings(request.user, request.DATA, username=username)
except (AccountUserNotFound, AccountNotAuthorized):
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
except AccountValidationError as err:
return Response({"field_errors": err.field_errors}, status=status.HTTP_400_BAD_REQUEST)
......
"""Python API for user profiles.
Profile information includes a student's demographic information and preferences,
but does NOT include basic account information such as username, password, and
email address.
"""
import datetime
import logging
from django.conf import settings
from django.db import IntegrityError
from pytz import UTC
import analytics
from eventtracking import tracker
from ..accounts import NAME_MIN_LENGTH
from ..accounts.api import get_account_settings
from ..models import User, UserPreference, UserOrgTag
from ..helpers import intercept_errors
log = logging.getLogger(__name__)
class ProfileRequestError(Exception):
""" The request to the API was not valid. """
pass
class ProfileUserNotFound(ProfileRequestError):
""" The requested user does not exist. """
pass
class ProfileInternalError(Exception):
""" An error occurred in an API call. """
pass
FULL_NAME_MAX_LENGTH = 255
FULL_NAME_MIN_LENGTH = NAME_MIN_LENGTH
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def preference_info(username):
"""Retrieve information about a user's preferences.
Arguments:
username (unicode): The username of the account to retrieve.
Returns:
dict: Empty if there is no user
"""
preferences = UserPreference.objects.filter(user__username=username)
preferences_dict = {}
for preference in preferences:
preferences_dict[preference.key] = preference.value
return preferences_dict
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def update_preferences(username, **kwargs):
"""Update a user's preferences.
Sets the provided preferences for the given user.
Arguments:
username (unicode): The username of the account to retrieve.
Keyword Arguments:
**kwargs (unicode): Arbitrary key-value preference pairs
Returns:
None
Raises:
ProfileUserNotFound
"""
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise ProfileUserNotFound
else:
for key, value in kwargs.iteritems():
UserPreference.set_preference(user, key, value)
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def update_email_opt_in(user, 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.
Arguments:
user (User): 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
"""
account_settings = get_account_settings(user)
year_of_birth = account_settings['year_of_birth']
of_age = (
year_of_birth is None or # If year of birth is not set, we assume user is of age.
datetime.datetime.now(UTC).year - 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()
if settings.FEATURES.get('SEGMENT_IO_LMS') and settings.SEGMENT_IO_LMS_KEY:
_track_update_email_opt_in(user.id, org, optin)
except IntegrityError as err:
log.warn(u"Could not update organization wide preference due to IntegrityError: {}".format(err.message))
def _track_update_email_opt_in(user_id, organization, opt_in):
"""Track an email opt-in preference change.
Arguments:
user_id (str): The ID of the user making the preference change.
organization (str): The organization whose emails are being opted into or out of by the user.
opt_in (Boolean): Whether the user has chosen to opt-in to emails from the organization.
Returns:
None
"""
event_name = 'edx.bi.user.org_email.opted_in' if opt_in else 'edx.bi.user.org_email.opted_out'
tracking_context = tracker.get_tracker().resolve_context()
analytics.track(
user_id,
event_name,
{
'category': 'communication',
'label': organization
},
context={
'Google Analytics': {
'clientId': tracking_context.get('client_id')
}
}
)
......@@ -4,7 +4,7 @@ Test the user course tag API.
from django.test import TestCase
from student.tests.factories import UserFactory
from openedx.core.djangoapps.user_api.api import course_tag as course_tag_api
from openedx.core.djangoapps.user_api.course_tag import api as course_tag_api
from opaque_keys.edx.locations import SlashSeparatedCourseKey
......
"""
Errors thrown by the various user APIs.
"""
class UserAPIRequestError(Exception):
"""There was a problem with the request to the User API. """
pass
class UserAPIInternalError(Exception):
"""An internal error occurred in the User API. """
pass
class UserNotFound(UserAPIRequestError):
"""The requested user does not exist. """
pass
class UserNotAuthorized(UserAPIRequestError):
"""The user is not authorized to perform the requested action. """
pass
class AccountRequestError(UserAPIRequestError):
"""There was a problem with the request to the account API. """
pass
class AccountUserAlreadyExists(AccountRequestError):
"""User with the same username and/or email already exists. """
pass
class AccountUsernameInvalid(AccountRequestError):
"""The requested username is not in a valid format. """
pass
class AccountEmailInvalid(AccountRequestError):
"""The requested email is not in a valid format. """
pass
class AccountPasswordInvalid(AccountRequestError):
"""The requested password is not in a valid format. """
pass
class AccountUpdateError(AccountRequestError):
"""
An update to the account failed. More detailed information is present in developer_message,
and depending on the type of error encountered, there may also be a non-null user_message field.
"""
def __init__(self, developer_message, user_message=None):
self.developer_message = developer_message
self.user_message = user_message
class AccountValidationError(AccountRequestError):
"""
Validation issues were found with the supplied data. More detailed information is present in field_errors,
a dict with specific information about each field that failed validation. For each field,
there will be at least a developer_message describing the validation issue, and possibly
also a user_message.
"""
def __init__(self, field_errors):
self.field_errors = field_errors
class PreferenceRequestError(UserAPIRequestError):
"""There was a problem with a preference request."""
pass
class PreferenceValidationError(PreferenceRequestError):
"""
Validation issues were found with the supplied data. More detailed information is present
in preference_errors, a dict with specific information about each preference that failed
validation. For each preference, there will be at least a developer_message describing
the validation issue, and possibly also a user_message.
"""
def __init__(self, preference_errors):
self.preference_errors = preference_errors
class PreferenceUpdateError(PreferenceRequestError):
"""
An update to a user preference failed. More detailed information is present in developer_message,
and depending on the type of error encountered, there may also be a non-null user_message field.
"""
def __init__(self, developer_message, user_message=None):
self.developer_message = developer_message
self.user_message = user_message
......@@ -45,9 +45,20 @@ def intercept_errors(api_error, ignore_errors=None):
try:
return func(*args, **kwargs)
except Exception as ex:
# Raise the original exception if it's in our list of "ignored" errors
# Raise and log the original exception if it's in our list of "ignored" errors
for ignored in ignore_errors or []:
if isinstance(ex, ignored):
msg = (
u"A handled error occurred when calling '{func_name}' "
u"with arguments '{args}' and keyword arguments '{kwargs}': "
u"{exception}"
).format(
func_name=func.func_name,
args=args,
kwargs=kwargs,
exception=repr(ex)
)
LOGGER.warning(msg)
raise
# Otherwise, log the error and raise the API-specific error
......
......@@ -19,7 +19,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
from student.models import CourseEnrollment
import openedx.core.djangoapps.user_api.api.profile as profile_api
from openedx.core.djangoapps.user_api.preferences.api import update_email_opt_in
from openedx.core.djangoapps.user_api.models import UserOrgTag
from openedx.core.djangoapps.user_api.management.commands import email_opt_in_list
......@@ -297,7 +297,7 @@ class EmailOptInListTest(ModuleStoreTestCase):
None
"""
profile_api.update_email_opt_in(user, org, is_opted_in)
update_email_opt_in(user, org, is_opted_in)
def _latest_pref_set_datetime(self, user):
"""Retrieve the latest opt-in preference for the user,
......
......@@ -25,27 +25,26 @@ class UserPreference(models.Model):
unique_together = ("user", "key")
@classmethod
def set_preference(cls, user, preference_key, preference_value):
"""
Sets the user preference for a given key
"""
user_pref, _ = cls.objects.get_or_create(user=user, key=preference_key)
user_pref.value = preference_value
user_pref.save()
def get_value(cls, user, preference_key):
"""Gets the user preference value for a given key.
@classmethod
def get_preference(cls, user, preference_key, default=None):
"""
Gets the user preference value for a given key
Note:
This method provides no authorization of access to the user preference.
Consider using user_api.preferences.api.get_user_preference instead if
this is part of a REST API request.
Returns the given default if there isn't a preference for the given key
"""
Arguments:
user (User): The user whose preference should be set.
preference_key (string): The key for the user preference.
Returns:
The user preference value, or None if one is not set.
"""
try:
user_pref = cls.objects.get(user=user, key=preference_key)
return user_pref.value
user_preference = cls.objects.get(user=user, key=preference_key)
return user_preference.value
except cls.DoesNotExist:
return default
return None
class UserCourseTag(models.Model):
......
......@@ -3,7 +3,7 @@ Provides partition support to the user service.
"""
import logging
import random
import api.course_tag as course_tag_api
import course_tag.api as course_tag_api
from xmodule.partitions.partitions import UserPartitionError, NoSuchUserPartitionGroupError
......
"""
NOTE: this API is WIP and has not yet been approved. Do not use this API without talking to Christina or Andy.
For more information, see:
https://openedx.atlassian.net/wiki/display/TNL/User+API
"""
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
from rest_framework import permissions
from django.utils.translation import ugettext as _
from openedx.core.lib.api.parsers import MergePatchParser
from ..errors import UserNotFound, UserNotAuthorized, PreferenceValidationError, PreferenceUpdateError
from .api import (
get_user_preference, get_user_preferences, set_user_preference, update_user_preferences, delete_user_preference
)
class PreferencesView(APIView):
"""
**Use Cases**
Get or update the user's preference information. Updates are only supported through merge patch.
Preference values of null in a patch request are treated as requests to remove the preference.
**Example Requests**:
GET /api/user/v0/preferences/{username}/
PATCH /api/user/v0/preferences/{username}/ with content_type "application/merge-patch+json"
**Response Value for GET**
A JSON dictionary will be returned with key/value pairs (all of type String).
If a user without "is_staff" access has requested preferences for a different user,
this method returns a 404.
If the specified username does not exist, this method returns a 404.
**Response for PATCH**
Users can only modify their own preferences. If the requesting user does not have username
"username", this method will return with a status of 404.
This method will also return a 404 if no user exists with username "username".
If "application/merge-patch+json" is not the specified content_type, this method returns a 415 status.
If the update could not be completed due to validation errors, this method returns a 400 with all
preference-specific error messages in the "field_errors" field of the returned JSON.
If the update could not be completed due to failure at the time of update, this method returns a 400 with
specific errors in the returned JSON.
If the update is successful, a 204 status is returned with no additional content.
"""
authentication_classes = (OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser)
permission_classes = (permissions.IsAuthenticated,)
parser_classes = (MergePatchParser,)
def get(self, request, username):
"""
GET /api/user/v0/preferences/{username}/
"""
try:
user_preferences = get_user_preferences(request.user, username=username)
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(user_preferences)
def patch(self, request, username):
"""
PATCH /api/user/v0/preferences/{username}/
"""
if not request.DATA or not getattr(request.DATA, "keys", None):
error_message = _("No data provided for user preference update")
return Response(
{
"developer_message": error_message,
"user_message": error_message
},
status=status.HTTP_400_BAD_REQUEST
)
try:
update_user_preferences(request.user, request.DATA, username=username)
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
except PreferenceValidationError as error:
return Response(
{"field_errors": error.preference_errors},
status=status.HTTP_400_BAD_REQUEST
)
except PreferenceUpdateError as error:
return Response(
{
"developer_message": error.developer_message,
"user_message": error.user_message
},
status=status.HTTP_400_BAD_REQUEST
)
return Response(status=status.HTTP_204_NO_CONTENT)
class PreferencesDetailView(APIView):
"""
**Use Cases**
Get, create, update, or delete a specific user preference.
**Example Requests**:
GET /api/user/v0/preferences/{username}/{preference_key}
PUT /api/user/v0/preferences/{username}/{preference_key}
DELETE /api/user/v0/preferences/{username}/{preference_key}
**Response Values for GET**
The preference value will be returned as a JSON string.
If a user without "is_staff" access has requested preferences for a different user,
this method returns a 404.
If the specified username or preference does not exist, this method returns a 404.
**Response Values for PUT**
A successful put returns a 204 and no content.
If the specified username or preference does not exist, this method returns a 404.
**Response for DELETE**
A successful delete returns a 204 and no content.
If the specified username or preference does not exist, this method returns a 404.
"""
authentication_classes = (OAuth2AuthenticationAllowInactiveUser, SessionAuthenticationAllowInactiveUser)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, username, preference_key):
"""
GET /api/user/v0/preferences/{username}/{preference_key}
"""
try:
value = get_user_preference(request.user, preference_key, username=username)
# There was no preference with that key, raise a 404.
if value is None:
return Response(status=status.HTTP_404_NOT_FOUND)
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(value)
def put(self, request, username, preference_key):
"""
PUT /api/user/v0/preferences/{username}/{preference_key}
"""
try:
set_user_preference(request.user, preference_key, request.DATA, username=username)
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
except PreferenceValidationError as error:
return Response(
{
"developer_message": error.preference_errors[preference_key]["developer_message"],
"user_message": error.preference_errors[preference_key]["user_message"]
},
status=status.HTTP_400_BAD_REQUEST
)
except PreferenceUpdateError as error:
return Response(
{
"developer_message": error.developer_message,
"user_message": error.user_message
},
status=status.HTTP_400_BAD_REQUEST
)
return Response(status=status.HTTP_204_NO_CONTENT)
def delete(self, request, username, preference_key):
"""
DELETE /api/user/v0/preferences/{username}/{preference_key}
"""
try:
preference_existed = delete_user_preference(request.user, preference_key, username=username)
except (UserNotFound, UserNotAuthorized):
return Response(status=status.HTTP_404_NOT_FOUND)
except PreferenceUpdateError as error:
return Response(
{
"developer_message": error.developer_message,
"user_message": error.user_message
},
status=status.HTTP_400_BAD_REQUEST
)
if not preference_existed:
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(status=status.HTTP_204_NO_CONTENT)
......@@ -29,3 +29,13 @@ class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = UserPreference
depth = 1
class RawUserPreferenceSerializer(serializers.ModelSerializer):
"""Serializer that generates a raw representation of a user preference.
"""
user = serializers.PrimaryKeyRelatedField()
class Meta:
model = UserPreference
depth = 1
# -*- coding: utf-8 -*-
""" Tests for the account API. """
import re
from unittest import skipUnless
from nose.tools import raises
from mock import patch
import ddt
from dateutil.parser import parse as parse_datetime
from django.core import mail
from django.test import TestCase
from django.conf import settings
from ..api import account as account_api
from ..models import UserProfile
@ddt.ddt
class AccountApiTest(TestCase):
USERNAME = u'frank-underwood'
PASSWORD = u'ṕáśśẃőŕd'
EMAIL = u'frank+underwood@example.com'
ORIG_HOST = 'example.com'
IS_SECURE = False
INVALID_USERNAMES = [
None,
u'',
u'a',
u'a' * (account_api.USERNAME_MAX_LENGTH + 1),
u'invalid_symbol_@',
u'invalid-unicode_fŕáńḱ',
]
INVALID_EMAILS = [
None,
u'',
u'a',
'no_domain',
'no+domain',
'@',
'@domain.com',
'test@no_extension',
u'fŕáńḱ@example.com',
u'frank@éxáḿṕĺé.ćőḿ',
# Long email -- subtract the length of the @domain
# except for one character (so we exceed the max length limit)
u'{user}@example.com'.format(
user=(u'e' * (account_api.EMAIL_MAX_LENGTH - 11))
)
]
INVALID_PASSWORDS = [
None,
u'',
u'a',
u'a' * (account_api.PASSWORD_MAX_LENGTH + 1)
]
def test_activate_account(self):
# Create the account, which is initially inactive
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
account = account_api.account_info(self.USERNAME)
self.assertEqual(account, {
'username': self.USERNAME,
'email': self.EMAIL,
'is_active': False
})
# Activate the account and verify that it is now active
account_api.activate_account(activation_key)
account = account_api.account_info(self.USERNAME)
self.assertTrue(account['is_active'])
def test_create_account_duplicate_username(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
with self.assertRaises(account_api.AccountUserAlreadyExists):
account_api.create_account(self.USERNAME, self.PASSWORD, 'different+email@example.com')
# Email uniqueness constraints were introduced in a database migration,
# which we disable in the unit tests to improve the speed of the test suite.
@skipUnless(settings.SOUTH_TESTS_MIGRATE, "South migrations required")
def test_create_account_duplicate_email(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
with self.assertRaises(account_api.AccountUserAlreadyExists):
account_api.create_account('different_user', self.PASSWORD, self.EMAIL)
def test_username_too_long(self):
long_username = 'e' * (account_api.USERNAME_MAX_LENGTH + 1)
with self.assertRaises(account_api.AccountUsernameInvalid):
account_api.create_account(long_username, self.PASSWORD, self.EMAIL)
def test_account_info_no_user(self):
self.assertIs(account_api.account_info('does_not_exist'), None)
@raises(account_api.AccountEmailInvalid)
@ddt.data(*INVALID_EMAILS)
def test_create_account_invalid_email(self, invalid_email):
account_api.create_account(self.USERNAME, self.PASSWORD, invalid_email)
@raises(account_api.AccountPasswordInvalid)
@ddt.data(*INVALID_PASSWORDS)
def test_create_account_invalid_password(self, invalid_password):
account_api.create_account(self.USERNAME, invalid_password, self.EMAIL)
@raises(account_api.AccountPasswordInvalid)
def test_create_account_username_password_equal(self):
# Username and password cannot be the same
account_api.create_account(self.USERNAME, self.USERNAME, self.EMAIL)
@raises(account_api.AccountRequestError)
@ddt.data(*INVALID_USERNAMES)
def test_create_account_invalid_username(self, invalid_username):
account_api.create_account(invalid_username, self.PASSWORD, self.EMAIL)
@raises(account_api.AccountNotAuthorized)
def test_activate_account_invalid_key(self):
account_api.activate_account(u'invalid')
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
def test_request_password_change(self):
# Create and activate an account
activation_key = account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
account_api.activate_account(activation_key)
# Request a password change
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
# Verify that one email message has been sent
self.assertEqual(len(mail.outbox), 1)
# Verify that the body of the message contains something that looks
# like an activation link
email_body = mail.outbox[0].body
result = re.search('(?P<url>https?://[^\s]+)', email_body)
self.assertIsNot(result, None)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
def test_request_password_change_invalid_user(self):
with self.assertRaises(account_api.AccountUserNotFound):
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
# Verify that no email messages have been sent
self.assertEqual(len(mail.outbox), 0)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in LMS')
def test_request_password_change_inactive_user(self):
# Create an account, but do not activate it
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
account_api.request_password_change(self.EMAIL, self.ORIG_HOST, self.IS_SECURE)
# Verify that the activation email was still sent
self.assertEqual(len(mail.outbox), 1)
def _assert_is_datetime(self, timestamp):
if not timestamp:
return False
try:
parse_datetime(timestamp)
except ValueError:
return False
else:
return True
......@@ -5,6 +5,7 @@ from student.tests.factories import UserFactory
from ..tests.factories import UserPreferenceFactory, UserCourseTagFactory, UserOrgTagFactory
from ..models import UserPreference
from ..preferences.api import set_user_preference
class UserPreferenceModelTest(ModuleStoreTestCase):
......@@ -67,20 +68,18 @@ class UserPreferenceModelTest(ModuleStoreTestCase):
self.assertEquals(tag.value, "barfoo")
self.assertNotEqual(original_modified, tag.modified)
def test_get_set_preference(self):
# Checks that you can set a preference and get that preference later
# Also, tests that no preference is returned for keys that are not set
def test_get_value(self):
"""Verifies the behavior of get_value."""
user = UserFactory.create()
key = 'testkey'
value = 'testvalue'
# does a round trip
UserPreference.set_preference(user, key, value)
pref = UserPreference.get_preference(user, key)
set_user_preference(user, key, value)
pref = UserPreference.get_value(user, key)
self.assertEqual(pref, value)
# get preference for key that doesn't exist for user
pref = UserPreference.get_preference(user, 'testkey_none')
pref = UserPreference.get_value(user, 'testkey_none')
self.assertIsNone(pref)
# -*- coding: utf-8 -*-
""" Tests for the profile API. """
from django.contrib.auth.models import User
import ddt
from django.test.utils import override_settings
from nose.tools import raises
from dateutil.parser import parse as parse_datetime
from pytz import UTC
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
import datetime
from ..accounts.api import get_account_settings
from ..api import account as account_api
from ..api import profile as profile_api
from ..models import UserProfile, UserOrgTag
@ddt.ddt
class ProfileApiTest(ModuleStoreTestCase):
USERNAME = u'frank-underwood'
PASSWORD = u'ṕáśśẃőŕd'
EMAIL = u'frank+underwood@example.com'
def test_create_profile(self):
# Create a new account, which should have an empty profile by default.
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
# Retrieve the account settings
user = User.objects.get(username=self.USERNAME)
account_settings = get_account_settings(user)
# Expect a date joined field but remove it to simplify the following comparison
self.assertIsNotNone(account_settings['date_joined'])
del account_settings['date_joined']
# Expect all the values to be defaulted
self.assertEqual(account_settings, {
'username': self.USERNAME,
'email': self.EMAIL,
'name': u'',
'gender': None,
'language': u'',
'goals': None,
'level_of_education': None,
'mailing_address': None,
'year_of_birth': None,
'country': None,
})
def test_update_and_retrieve_preference_info(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_preferences(self.USERNAME, preference_key='preference_value')
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 14 years old can opt-in
(14, True, u"True"),
# Check that someone 13 years old cannot opt-in (must have turned 13 before this year)
(13, True, u"False"),
# 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(user, 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(user, 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(UTC).year - age # pylint: disable=maybe-no-member
profile.year_of_birth = year_of_birth
profile.save()
profile_api.update_email_opt_in(user, course.id.org, option)
profile_api.update_email_opt_in(user, 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)
self.assertEqual(preferences, {})
profile_api.update_preferences(self.USERNAME, preference_key='preference_value')
def test_update_and_retrieve_preference_info_unicode(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_preferences(self.USERNAME, **{u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ': u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd'})
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences[u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ'], u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd')
def _assert_is_datetime(self, timestamp):
if not timestamp:
return False
try:
parse_datetime(timestamp)
except ValueError:
return False
else:
return True
......@@ -25,7 +25,10 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
from third_party_auth.tests.testutil import simulate_running_pipeline
from ..accounts.api import get_account_settings
from ..api import account as account_api, profile as profile_api
from ..accounts import (
NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
)
from ..models import UserOrgTag
from ..tests.factories import UserPreferenceFactory
from ..tests.test_constants import SORTED_COUNTRIES
......@@ -618,8 +621,8 @@ class LoginSessionViewTest(ApiTestCase):
platform_name=settings.PLATFORM_NAME
),
"restrictions": {
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH
},
"errorMessages": {},
},
......@@ -632,8 +635,8 @@ class LoginSessionViewTest(ApiTestCase):
"placeholder": "",
"instructions": "",
"restrictions": {
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH
},
"errorMessages": {},
}
......@@ -769,8 +772,8 @@ class PasswordResetViewTest(ApiTestCase):
platform_name=settings.PLATFORM_NAME
),
"restrictions": {
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH
},
"errorMessages": {},
}
......@@ -827,8 +830,8 @@ class RegistrationViewTest(ApiTestCase):
u"label": u"Email",
u"placeholder": u"username@domain.com",
u"restrictions": {
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH
},
}
)
......@@ -842,7 +845,7 @@ class RegistrationViewTest(ApiTestCase):
u"label": u"Full name",
u"instructions": u"The name that will appear on your certificates",
u"restrictions": {
"max_length": profile_api.FULL_NAME_MAX_LENGTH,
"max_length": NAME_MAX_LENGTH,
},
}
)
......@@ -856,8 +859,8 @@ class RegistrationViewTest(ApiTestCase):
u"label": u"Public username",
u"instructions": u"The name that will identify you in your courses",
u"restrictions": {
"min_length": account_api.USERNAME_MIN_LENGTH,
"max_length": account_api.USERNAME_MAX_LENGTH
"min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH
},
}
)
......@@ -870,8 +873,8 @@ class RegistrationViewTest(ApiTestCase):
u"required": True,
u"label": u"Password",
u"restrictions": {
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH
},
}
)
......@@ -905,8 +908,8 @@ class RegistrationViewTest(ApiTestCase):
u"label": u"Email",
u"placeholder": u"username@domain.com",
u"restrictions": {
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH
},
}
)
......@@ -922,7 +925,7 @@ class RegistrationViewTest(ApiTestCase):
u"label": u"Full name",
u"instructions": u"The name that will appear on your certificates",
u"restrictions": {
"max_length": profile_api.FULL_NAME_MAX_LENGTH,
"max_length": NAME_MAX_LENGTH,
}
}
)
......@@ -939,8 +942,8 @@ class RegistrationViewTest(ApiTestCase):
u"placeholder": u"",
u"instructions": u"The name that will identify you in your courses",
u"restrictions": {
"min_length": account_api.USERNAME_MIN_LENGTH,
"max_length": account_api.USERNAME_MAX_LENGTH
"min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH
}
}
)
......@@ -1237,20 +1240,13 @@ class RegistrationViewTest(ApiTestCase):
self.assertHttpOK(response)
self.assertIn(settings.EDXMKTG_COOKIE_NAME, self.client.cookies)
# Verify that the user exists
self.assertEqual(
account_api.account_info(self.USERNAME),
{
"username": self.USERNAME,
"email": self.EMAIL,
"is_active": False
}
)
# Verify that the user's full name is set
user = User.objects.get(username=self.USERNAME)
account_settings = get_account_settings(user)
self.assertEqual(account_settings["name"], self.NAME)
self.assertEqual(self.USERNAME, account_settings["username"])
self.assertEqual(self.EMAIL, account_settings["email"])
self.assertFalse(account_settings["is_active"])
self.assertEqual(self.NAME, account_settings["name"])
# Verify that we've been logged in
# by trying to access a page that requires authentication
......
......@@ -3,6 +3,7 @@ Defines the URL routes for this app.
"""
from .accounts.views import AccountView
from .preferences.views import PreferencesView, PreferencesDetailView
from django.conf.urls import patterns, url
......@@ -15,4 +16,14 @@ urlpatterns = patterns(
AccountView.as_view(),
name="accounts_api"
),
url(
r'^v0/preferences/' + USERNAME_PATTERN + '$',
PreferencesView.as_view(),
name="preferences_api"
),
url(
r'^v0/preferences/' + USERNAME_PATTERN + '/(?P<preference_key>[a-zA-Z0-9_]+)$',
PreferencesDetailView.as_view(),
name="preferences_detail_api"
),
)
......@@ -28,9 +28,14 @@ from edxmako.shortcuts import marketing_link
from student.views import create_account_with_params, set_marketing_cookie
from util.authentication import SessionAuthenticationAllowInactiveUser
from util.json_request import JsonResponse
from .api import account as account_api, profile as profile_api
from .preferences.api import update_email_opt_in
from .helpers import FormDescription, shim_student_view, require_post_params
from .models import UserPreference, UserProfile
from .accounts import (
NAME_MAX_LENGTH, EMAIL_MIN_LENGTH, EMAIL_MAX_LENGTH, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH
)
from .accounts.api import check_account_exists
from .serializers import UserSerializer, UserPreferenceSerializer
......@@ -79,8 +84,8 @@ class LoginSessionView(APIView):
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH,
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
}
)
......@@ -93,8 +98,8 @@ class LoginSessionView(APIView):
label=password_label,
field_type="password",
restrictions={
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH,
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH,
}
)
......@@ -251,7 +256,7 @@ class RegistrationView(APIView):
username = data.get('username')
# Handle duplicate email/username
conflicts = account_api.check_account_exists(email=email, username=username)
conflicts = check_account_exists(email=email, username=username)
if conflicts:
conflict_messages = {
# Translators: This message is shown to users who attempt to create a new
......@@ -321,8 +326,8 @@ class RegistrationView(APIView):
label=email_label,
placeholder=email_placeholder,
restrictions={
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH,
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
},
required=required
)
......@@ -350,7 +355,7 @@ class RegistrationView(APIView):
label=name_label,
instructions=name_instructions,
restrictions={
"max_length": profile_api.FULL_NAME_MAX_LENGTH,
"max_length": NAME_MAX_LENGTH,
},
required=required
)
......@@ -380,8 +385,8 @@ class RegistrationView(APIView):
label=username_label,
instructions=username_instructions,
restrictions={
"min_length": account_api.USERNAME_MIN_LENGTH,
"max_length": account_api.USERNAME_MAX_LENGTH,
"min_length": USERNAME_MIN_LENGTH,
"max_length": USERNAME_MAX_LENGTH,
},
required=required
)
......@@ -405,8 +410,8 @@ class RegistrationView(APIView):
label=password_label,
field_type="password",
restrictions={
"min_length": account_api.PASSWORD_MIN_LENGTH,
"max_length": account_api.PASSWORD_MAX_LENGTH,
"min_length": PASSWORD_MIN_LENGTH,
"max_length": PASSWORD_MAX_LENGTH,
},
required=required
)
......@@ -775,8 +780,8 @@ class PasswordResetView(APIView):
placeholder=email_placeholder,
instructions=email_instructions,
restrictions={
"min_length": account_api.EMAIL_MIN_LENGTH,
"max_length": account_api.EMAIL_MAX_LENGTH,
"min_length": EMAIL_MIN_LENGTH,
"max_length": EMAIL_MAX_LENGTH,
}
)
......@@ -870,5 +875,5 @@ class UpdateEmailOptInPreference(APIView):
)
# Only check for true. All other values are False.
email_opt_in = request.DATA['email_opt_in'].lower() == 'true'
profile_api.update_email_opt_in(request.user, org, email_opt_in)
update_email_opt_in(request.user, org, email_opt_in)
return HttpResponse(status=status.HTTP_200_OK)
......@@ -54,17 +54,7 @@ class IsUserInUrl(permissions.BasePermission):
def has_permission(self, request, view):
# Return a 404 instead of a 403 (Unauthorized). If one user is looking up
# other users, do not let them deduce the existence of an account.
if request.user.username != request.parser_context.get('kwargs', {}).get('username', None):
url_username = request.parser_context.get('kwargs', {}).get('username', '')
if request.user.username.lower() != url_username.lower():
raise Http404()
return True
class IsUserInUrlOrStaff(IsUserInUrl):
"""
Permission that checks to see if the request user matches the user in the URL or has is_staff access.
"""
def has_permission(self, request, view):
if request.user.is_staff:
return True
return super(IsUserInUrlOrStaff, self).has_permission(request, view)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment