Merge pull request #14051 from edx/vkaracic/SOL-2133
[SOL-2133] Add user deactivation endpoint.
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('student', '0008_auto_20161117_1209'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='userprofile',
|
||||
options={'permissions': (('can_deactivate_users', 'Can deactivate, but NOT delete users'),)},
|
||||
),
|
||||
]
|
||||
@@ -234,6 +234,7 @@ class UserProfile(models.Model):
|
||||
|
||||
class Meta(object):
|
||||
db_table = "auth_userprofile"
|
||||
permissions = (("can_deactivate_users", "Can deactivate, but NOT delete users"),)
|
||||
|
||||
# CRITICAL TODO/SECURITY
|
||||
# Sanitize all fields.
|
||||
|
||||
@@ -6,7 +6,8 @@ from student.models import (User, UserProfile, Registration,
|
||||
PendingEmailChange, UserStanding,
|
||||
CourseAccessRole)
|
||||
from course_modes.models import CourseMode
|
||||
from django.contrib.auth.models import Group, AnonymousUser
|
||||
from django.contrib.auth.models import AnonymousUser, Group, Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from datetime import datetime
|
||||
import factory
|
||||
from factory import lazy_attribute
|
||||
@@ -18,6 +19,8 @@ from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
# Factories are self documenting
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
TEST_PASSWORD = 'test'
|
||||
|
||||
|
||||
class GroupFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
@@ -123,6 +126,10 @@ class AdminFactory(UserFactory):
|
||||
is_staff = True
|
||||
|
||||
|
||||
class SuperuserFactory(UserFactory):
|
||||
is_superuser = True
|
||||
|
||||
|
||||
class CourseEnrollmentFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = CourseEnrollment
|
||||
@@ -161,3 +168,18 @@ class PendingEmailChangeFactory(DjangoModelFactory):
|
||||
user = factory.SubFactory(UserFactory)
|
||||
new_email = factory.Sequence(u'new+email+{0}@edx.org'.format)
|
||||
activation_key = factory.Sequence(u'{:0<30d}'.format)
|
||||
|
||||
|
||||
class ContentTypeFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = ContentType
|
||||
|
||||
app_label = factory.Faker('app_name')
|
||||
|
||||
|
||||
class PermissionFactory(DjangoModelFactory):
|
||||
class Meta(object):
|
||||
model = Permission
|
||||
|
||||
codename = factory.Faker('codename')
|
||||
content_type = factory.SubFactory(ContentTypeFactory)
|
||||
|
||||
15
openedx/core/djangoapps/user_api/accounts/permissions.py
Normal file
15
openedx/core/djangoapps/user_api/accounts/permissions.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Permissions classes for User accounts API views.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from rest_framework import permissions
|
||||
|
||||
|
||||
class CanDeactivateUser(permissions.BasePermission):
|
||||
"""
|
||||
Grants access to AccountDeactivationView if the requesting user is a superuser
|
||||
or has the explicit permission to deactivate a User account.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
return request.user.has_perm('student.can_deactivate_users')
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
Tests for User deactivation API permissions
|
||||
"""
|
||||
from django.test import TestCase, RequestFactory
|
||||
|
||||
from openedx.core.djangoapps.user_api.accounts.permissions import CanDeactivateUser
|
||||
from student.tests.factories import ContentTypeFactory, PermissionFactory, SuperuserFactory, UserFactory
|
||||
|
||||
|
||||
class CanDeactivateUserTest(TestCase):
|
||||
""" Tests for user deactivation API permissions """
|
||||
|
||||
def setUp(self):
|
||||
super(CanDeactivateUserTest, self).setUp()
|
||||
self.request = RequestFactory().get('/test/url')
|
||||
|
||||
def test_api_permission_superuser(self):
|
||||
self.request.user = SuperuserFactory()
|
||||
|
||||
result = CanDeactivateUser().has_permission(self.request, None)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_api_permission_user_granted_permission(self):
|
||||
user = UserFactory()
|
||||
permission = PermissionFactory(
|
||||
codename='can_deactivate_users',
|
||||
content_type=ContentTypeFactory(
|
||||
app_label='student'
|
||||
)
|
||||
)
|
||||
user.user_permissions.add(permission) # pylint: disable=no-member
|
||||
self.request.user = user
|
||||
|
||||
result = CanDeactivateUser().has_permission(self.request, None)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_api_permission_user_without_permission(self):
|
||||
self.request.user = UserFactory()
|
||||
result = CanDeactivateUser().has_permission(self.request, None)
|
||||
self.assertFalse(result)
|
||||
@@ -2,30 +2,35 @@
|
||||
"""
|
||||
Test cases to cover Accounts-related behaviors of the User API application
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
import datetime
|
||||
import ddt
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from copy import deepcopy
|
||||
from mock import patch
|
||||
from nose.plugins.attrib import attr
|
||||
from pytz import UTC
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from django.test.utils import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from openedx.core.djangoapps.user_api.models import UserPreference
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import UserProfile, LanguageProficiency, PendingEmailChange
|
||||
from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY
|
||||
from openedx.core.djangoapps.user_api.accounts import ACCOUNT_VISIBILITY_PREF_KEY
|
||||
from openedx.core.djangoapps.user_api.models import UserPreference
|
||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
||||
from openedx.core.djangolib.testing.utils import CacheIsolationTestCase, skip_unless_lms
|
||||
from .. import PRIVATE_VISIBILITY, ALL_USERS_VISIBILITY
|
||||
from student.models import UserProfile, LanguageProficiency, PendingEmailChange
|
||||
from student.tests.factories import (
|
||||
AdminFactory, ContentTypeFactory, TEST_PASSWORD, PermissionFactory, SuperuserFactory, UserFactory
|
||||
)
|
||||
|
||||
TEST_PROFILE_IMAGE_UPLOADED_AT = datetime.datetime(2002, 1, 9, 15, 43, 01, tzinfo=UTC)
|
||||
|
||||
@@ -40,23 +45,22 @@ class UserAPITestCase(APITestCase):
|
||||
"""
|
||||
The base class for all tests of the User API
|
||||
"""
|
||||
test_password = "test"
|
||||
|
||||
def setUp(self):
|
||||
super(UserAPITestCase, self).setUp()
|
||||
|
||||
self.anonymous_client = APIClient()
|
||||
self.different_user = UserFactory.create(password=self.test_password)
|
||||
self.different_user = UserFactory.create(password=TEST_PASSWORD)
|
||||
self.different_client = APIClient()
|
||||
self.staff_user = UserFactory(is_staff=True, password=self.test_password)
|
||||
self.staff_user = UserFactory(is_staff=True, password=TEST_PASSWORD)
|
||||
self.staff_client = APIClient()
|
||||
self.user = UserFactory.create(password=self.test_password) # will be assigned to self.client by default
|
||||
self.user = UserFactory.create(password=TEST_PASSWORD) # will be assigned to self.client by default
|
||||
|
||||
def login_client(self, api_client, user):
|
||||
"""Helper method for getting the client and user and logging in. Returns client. """
|
||||
client = getattr(self, api_client)
|
||||
user = getattr(self, user)
|
||||
client.login(username=user.username, password=self.test_password)
|
||||
client.login(username=user.username, password=TEST_PASSWORD)
|
||||
return client
|
||||
|
||||
def send_patch(self, client, json_data, content_type="application/merge-patch+json", expected_status=200):
|
||||
@@ -168,7 +172,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) can get her own username.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self._verify_get_own_username(15)
|
||||
|
||||
def test_get_username_inactive(self):
|
||||
@@ -176,7 +180,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
Test that a logged-in client can get their
|
||||
username, even if inactive.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
self._verify_get_own_username(15)
|
||||
@@ -271,7 +275,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
"""
|
||||
Test that DELETE, POST, and PUT are not supported.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.assertEqual(405, self.client.put(self.url).status_code)
|
||||
self.assertEqual(405, self.client.post(self.url).status_code)
|
||||
self.assertEqual(405, self.client.delete(self.url).status_code)
|
||||
@@ -298,7 +302,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
Test that a client (logged in) can only get the shareable fields for a different user.
|
||||
This is the case when default_visibility is set to "all_users".
|
||||
"""
|
||||
self.different_client.login(username=self.different_user.username, password=self.test_password)
|
||||
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
|
||||
self.create_mock_profile(self.user)
|
||||
with self.assertNumQueries(19):
|
||||
response = self.send_get(self.different_client)
|
||||
@@ -313,7 +317,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
Test that a client (logged in) can only get the shareable fields for a different user.
|
||||
This is the case when default_visibility is set to "private".
|
||||
"""
|
||||
self.different_client.login(username=self.different_user.username, password=self.test_password)
|
||||
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
|
||||
self.create_mock_profile(self.user)
|
||||
with self.assertNumQueries(19):
|
||||
response = self.send_get(self.different_client)
|
||||
@@ -389,7 +393,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
# Badges aren't on by default, so should not be present.
|
||||
self.assertEqual(False, data["accomplishments_shared"])
|
||||
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
verify_get_own_information(17)
|
||||
|
||||
# Now make sure that the user can get the same information, even if not active
|
||||
@@ -408,7 +412,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
legacy_profile.bio = ""
|
||||
legacy_profile.save()
|
||||
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
with self.assertNumQueries(17):
|
||||
response = self.send_get(self.client)
|
||||
for empty_field in ("level_of_education", "gender", "country", "bio"):
|
||||
@@ -499,7 +503,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
|
||||
def test_patch_inactive_user(self):
|
||||
""" Verify that a user can patch her own account, even if inactive. """
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
response = self.send_patch(self.client, {"goals": "to not activate account"})
|
||||
@@ -541,7 +545,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
"""
|
||||
Test the behavior of patch when an incorrect content_type is specified.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.send_patch(self.client, {}, content_type="application/json", expected_status=415)
|
||||
self.send_patch(self.client, {}, content_type="application/xml", expected_status=415)
|
||||
|
||||
@@ -550,7 +554,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
Tests the behavior of patch when attempting to set fields with a select list of options to the empty string.
|
||||
Also verifies the behaviour when setting to None.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
for field_name in ["gender", "level_of_education", "country"]:
|
||||
response = self.send_patch(self.client, {field_name: ""})
|
||||
# Although throwing a 400 might be reasonable, the default DRF behavior with ModelSerializer
|
||||
@@ -586,7 +590,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
get_response = self.send_get(self.client)
|
||||
self.assertEqual(new_name, get_response.data["name"])
|
||||
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
legacy_profile = UserProfile.objects.get(id=self.user.id)
|
||||
self.assertEqual({}, legacy_profile.get_meta())
|
||||
old_name = legacy_profile.name
|
||||
@@ -706,7 +710,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
Test that AccountUpdateErrors are passed through to the response.
|
||||
"""
|
||||
serializer_save.side_effect = [Exception("bummer"), None]
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
error_response = self.send_patch(self.client, {"goals": "save an account field"}, expected_status=400)
|
||||
self.assertEqual(
|
||||
"Error thrown when saving account updates: 'bummer'",
|
||||
@@ -721,7 +725,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
|
||||
with a '/', the API generates the full URL to profile images based on
|
||||
the URL of the request.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_get(self.client)
|
||||
self.assertEqual(
|
||||
response.data["profile_image"],
|
||||
@@ -787,12 +791,11 @@ class TestAccountAPITransactions(TransactionTestCase):
|
||||
"""
|
||||
Tests the transactional behavior of the account API
|
||||
"""
|
||||
test_password = "test"
|
||||
|
||||
def setUp(self):
|
||||
super(TestAccountAPITransactions, self).setUp()
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory.create(password=self.test_password)
|
||||
self.user = UserFactory.create(password=TEST_PASSWORD)
|
||||
self.url = reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
|
||||
@patch('student.views.do_email_change_request')
|
||||
@@ -804,7 +807,7 @@ class TestAccountAPITransactions(TransactionTestCase):
|
||||
# Throw an error from the method that is used to process the email change request
|
||||
# (this is the last thing done in the api method). Verify that the profile did not change.
|
||||
mock_email_change.side_effect = [ValueError, "mock value error thrown"]
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
old_email = self.user.email
|
||||
|
||||
json_data = {"email": "foo@bar.com", "gender": "o"}
|
||||
@@ -816,3 +819,65 @@ class TestAccountAPITransactions(TransactionTestCase):
|
||||
data = response.data
|
||||
self.assertEqual(old_email, data["email"])
|
||||
self.assertEqual(u"m", data["gender"])
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Account APIs are only supported in LMS')
|
||||
class TestAccountDeactivation(TestCase):
|
||||
"""
|
||||
Tests the account deactivation endpoint.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestAccountDeactivation, self).setUp()
|
||||
self.superuser = SuperuserFactory()
|
||||
self.staff_user = AdminFactory()
|
||||
self.test_user = UserFactory()
|
||||
self.url = reverse('accounts_deactivation', kwargs={'username': self.test_user.username})
|
||||
|
||||
def assert_activation_status(self, expected_status=status.HTTP_200_OK, expected_activation_status=False):
|
||||
"""
|
||||
Helper function for making a request to the deactivation endpoint, and asserting the status.
|
||||
|
||||
Args:
|
||||
expected_status(int): Expected request's response status.
|
||||
expected_activation_status(bool): Expected user has_usable_password attribute value.
|
||||
"""
|
||||
response = self.client.post(self.url)
|
||||
self.assertEqual(response.status_code, expected_status)
|
||||
self.test_user.refresh_from_db() # pylint: disable=no-member
|
||||
self.assertEqual(self.test_user.has_usable_password(), expected_activation_status) # pylint: disable=no-member
|
||||
|
||||
def test_superuser_deactivates_user(self):
|
||||
"""
|
||||
Verify a user is deactivated when a superuser posts to the deactivation endpoint.
|
||||
"""
|
||||
self.client.login(username=self.superuser.username, password=TEST_PASSWORD)
|
||||
self.assertTrue(self.test_user.has_usable_password()) # pylint: disable=no-member
|
||||
self.assert_activation_status()
|
||||
|
||||
def test_user_with_permission_deactivates_user(self):
|
||||
"""
|
||||
Verify a user is deactivated when a user with permission posts to the deactivation endpoint.
|
||||
"""
|
||||
user = UserFactory()
|
||||
permission = PermissionFactory(
|
||||
codename='can_deactivate_users',
|
||||
content_type=ContentTypeFactory(
|
||||
app_label='student'
|
||||
)
|
||||
)
|
||||
user.user_permissions.add(permission) # pylint: disable=no-member
|
||||
self.client.login(username=user.username, password=TEST_PASSWORD)
|
||||
self.assertTrue(self.test_user.has_usable_password()) # pylint: disable=no-member
|
||||
self.assert_activation_status()
|
||||
|
||||
def test_unauthorized_rejection(self):
|
||||
"""
|
||||
Verify unauthorized users cannot deactivate accounts.
|
||||
"""
|
||||
self.client.login(username=self.test_user.username, password=TEST_PASSWORD)
|
||||
self.assertTrue(self.test_user.has_usable_password()) # pylint: disable=no-member
|
||||
self.assert_activation_status(
|
||||
expected_status=status.HTTP_403_FORBIDDEN,
|
||||
expected_activation_status=True
|
||||
)
|
||||
|
||||
@@ -10,15 +10,18 @@ from edx_rest_framework_extensions.authentication import JwtAuthentication
|
||||
from rest_framework import permissions
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from .api import get_account_settings, update_account_settings
|
||||
from .permissions import CanDeactivateUser
|
||||
from ..errors import UserNotFound, UserNotAuthorized, AccountUpdateError, AccountValidationError
|
||||
from openedx.core.lib.api.authentication import (
|
||||
SessionAuthenticationAllowInactiveUser,
|
||||
OAuth2AuthenticationAllowInactiveUser,
|
||||
)
|
||||
from openedx.core.lib.api.parsers import MergePatchParser
|
||||
from .api import get_account_settings, update_account_settings
|
||||
from ..errors import UserNotFound, UserNotAuthorized, AccountUpdateError, AccountValidationError
|
||||
from student.models import User
|
||||
|
||||
|
||||
class AccountViewSet(ViewSet):
|
||||
@@ -219,3 +222,23 @@ class AccountViewSet(ViewSet):
|
||||
)
|
||||
|
||||
return Response(account_settings)
|
||||
|
||||
|
||||
class AccountDeactivationView(APIView):
|
||||
"""
|
||||
Account deactivation viewset. Currently only supports POST requests.
|
||||
Only admins can deactivate accounts.
|
||||
"""
|
||||
permission_classes = (permissions.IsAuthenticated, CanDeactivateUser)
|
||||
|
||||
def post(self, request, username):
|
||||
"""
|
||||
POST /api/user/v1/accounts/{username}/deactivate/
|
||||
|
||||
Marks the user as having no password set for deactivation purposes.
|
||||
"""
|
||||
user = User.objects.get(username=username)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
account_settings = get_account_settings(request, [username])[0]
|
||||
return Response(account_settings)
|
||||
|
||||
@@ -10,7 +10,7 @@ from mock import patch
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test.testcases import TransactionTestCase
|
||||
from rest_framework.test import APIClient
|
||||
from student.tests.factories import UserFactory
|
||||
from student.tests.factories import UserFactory, TEST_PASSWORD
|
||||
|
||||
from openedx.core.djangolib.testing.utils import skip_unless_lms
|
||||
from ...accounts.tests.test_views import UserAPITestCase
|
||||
@@ -42,7 +42,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that DELETE, POST, and PUT are not supported.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.assertEqual(405, self.client.put(self.url).status_code)
|
||||
self.assertEqual(405, self.client.post(self.url).status_code)
|
||||
self.assertEqual(405, self.client.delete(self.url).status_code)
|
||||
@@ -51,7 +51,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) cannot get the preferences information for a different client.
|
||||
"""
|
||||
self.different_client.login(username=self.different_user.username, password=self.test_password)
|
||||
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
|
||||
self.send_get(self.different_client, expected_status=404)
|
||||
|
||||
@ddt.data(
|
||||
@@ -72,7 +72,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
Test that a client (logged in) can get her own preferences information (verifying the default
|
||||
state before any preferences are stored).
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_get(self.client)
|
||||
self.assertEqual({}, response.data)
|
||||
|
||||
@@ -117,7 +117,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"""
|
||||
Test the behavior of patch when an incorrect content_type is specified.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.send_patch(self.client, {}, content_type="application/json", expected_status=415)
|
||||
self.send_patch(self.client, {}, content_type="application/xml", expected_status=415)
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"""
|
||||
Internal helper to generalize the creation of a set of preferences
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
if not is_active:
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
@@ -182,7 +182,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
set_user_preference(self.user, "time_zone", "Asia/Macau")
|
||||
|
||||
# Send the patch request
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.send_patch(
|
||||
self.client,
|
||||
{
|
||||
@@ -215,7 +215,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
set_user_preference(self.user, "time_zone", "Pacific/Midway")
|
||||
|
||||
# Send the patch request
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_patch(
|
||||
self.client,
|
||||
{
|
||||
@@ -266,7 +266,7 @@ class TestPreferencesAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) receives appropriate errors for a bad request.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
|
||||
# Verify a non-dict request
|
||||
response = self.send_patch(self.client, "non_dict_request", expected_status=400)
|
||||
@@ -325,7 +325,7 @@ class TestPreferencesAPITransactions(TransactionTestCase):
|
||||
def setUp(self):
|
||||
super(TestPreferencesAPITransactions, self).setUp()
|
||||
self.client = APIClient()
|
||||
self.user = UserFactory.create(password=self.test_password)
|
||||
self.user = UserFactory.create(password=TEST_PASSWORD)
|
||||
self.url = reverse("preferences_api", kwargs={'username': self.user.username})
|
||||
|
||||
@patch('openedx.core.djangoapps.user_api.models.UserPreference.delete')
|
||||
@@ -342,7 +342,7 @@ class TestPreferencesAPITransactions(TransactionTestCase):
|
||||
# after one of the updates has happened, in which case the whole operation
|
||||
# should be rolled back.
|
||||
delete_user_preference.side_effect = [Exception, None]
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
json_data = {
|
||||
"a": "2",
|
||||
"b": None,
|
||||
@@ -396,7 +396,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that POST and PATCH are not supported.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.assertEqual(405, self.client.post(self.url).status_code)
|
||||
self.assertEqual(405, self.client.patch(self.url).status_code)
|
||||
|
||||
@@ -404,7 +404,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) cannot manipulate a preference for a different client.
|
||||
"""
|
||||
self.different_client.login(username=self.different_user.username, password=self.test_password)
|
||||
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
|
||||
self.send_get(self.different_client, expected_status=404)
|
||||
self.send_put(self.different_client, "new_value", expected_status=404)
|
||||
self.send_delete(self.different_client, expected_status=404)
|
||||
@@ -429,7 +429,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
Test that a 404 is returned if the user does not have a preference with the given preference_key.
|
||||
"""
|
||||
self._set_url("does_not_exist")
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_get(self.client, expected_status=404)
|
||||
self.assertIsNone(response.data)
|
||||
|
||||
@@ -469,7 +469,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Generalization of the actual test workflow
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
if not is_active:
|
||||
self.user.is_active = False
|
||||
self.user.save()
|
||||
@@ -490,7 +490,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
Test that a client (logged in) cannot create an empty preference.
|
||||
"""
|
||||
self._set_url("new_key")
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_put(self.client, preference_value, expected_status=400)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
@@ -505,7 +505,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client cannot create preferences with bad keys
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
|
||||
too_long_preference_key = "x" * 256
|
||||
new_value = "new value"
|
||||
@@ -544,7 +544,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) can update a preference.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
self.send_put(self.client, preference_value)
|
||||
response = self.send_get(self.client)
|
||||
self.assertEqual(unicode(preference_value), response.data)
|
||||
@@ -572,7 +572,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) cannot update a preference to null.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_put(self.client, preference_value, expected_status=400)
|
||||
self.assertEqual(
|
||||
response.data,
|
||||
@@ -588,7 +588,7 @@ class TestPreferencesDetailAPI(UserAPITestCase):
|
||||
"""
|
||||
Test that a client (logged in) can delete her own preference.
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=self.test_password)
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
|
||||
# Verify that a preference can be deleted
|
||||
self.send_delete(self.client)
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.conf import settings
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from ..profile_images.views import ProfileImageView
|
||||
from .accounts.views import AccountViewSet
|
||||
from .accounts.views import AccountDeactivationView, AccountViewSet
|
||||
from .preferences.views import PreferencesView, PreferencesDetailView
|
||||
from .verification_api.views import PhotoVerificationStatusView
|
||||
|
||||
@@ -33,6 +33,11 @@ urlpatterns = patterns(
|
||||
ProfileImageView.as_view(),
|
||||
name='accounts_profile_image_api'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/deactivate/$'.format(settings.USERNAME_PATTERN),
|
||||
AccountDeactivationView.as_view(),
|
||||
name='accounts_deactivation'
|
||||
),
|
||||
url(
|
||||
r'^v1/accounts/{}/verification_status/$'.format(settings.USERNAME_PATTERN),
|
||||
PhotoVerificationStatusView.as_view(),
|
||||
|
||||
Reference in New Issue
Block a user