Merge pull request #7053 from edx/christina/account-api
User account API
This commit is contained in:
@@ -105,7 +105,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
|
||||
"""
|
||||
def test_invalid_user(self):
|
||||
self.login_and_enroll()
|
||||
self.api_response(expected_response_code=403, username='no_user')
|
||||
self.api_response(expected_response_code=404, username='no_user')
|
||||
|
||||
def test_other_user(self):
|
||||
# login and enroll as the test user
|
||||
@@ -120,7 +120,7 @@ class MobileAuthUserTestMixin(MobileAuthTestMixin):
|
||||
|
||||
# now login and call the API as the test user
|
||||
self.login()
|
||||
self.api_response(expected_response_code=403, username=other.username)
|
||||
self.api_response(expected_response_code=404, username=other.username)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
|
||||
@@ -10,6 +10,7 @@ from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2Au
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from courseware.courses import get_course_with_access
|
||||
from openedx.core.lib.api.permissions import IsUserInUrl
|
||||
|
||||
|
||||
def mobile_course_access(depth=0, verify_enrolled=True):
|
||||
@@ -42,13 +43,6 @@ def mobile_view(is_user=False):
|
||||
"""
|
||||
Function and class decorator that abstracts the authentication and permission checks for mobile api views.
|
||||
"""
|
||||
class IsUser(permissions.BasePermission):
|
||||
"""
|
||||
Permission that checks to see if the request user matches the user in the URL.
|
||||
"""
|
||||
def has_permission(self, request, view):
|
||||
return request.user.username == request.parser_context.get('kwargs', {}).get('username', None)
|
||||
|
||||
def _decorator(func_or_class):
|
||||
"""
|
||||
Requires either OAuth2 or Session-based authentication.
|
||||
@@ -60,6 +54,6 @@ def mobile_view(is_user=False):
|
||||
)
|
||||
func_or_class.permission_classes = (permissions.IsAuthenticated,)
|
||||
if is_user:
|
||||
func_or_class.permission_classes += (IsUser,)
|
||||
func_or_class.permission_classes += (IsUserInUrl,)
|
||||
return func_or_class
|
||||
return _decorator
|
||||
|
||||
@@ -61,6 +61,8 @@ urlpatterns = ('', # nopep8
|
||||
|
||||
url(r'^user_api/', include('openedx.core.djangoapps.user_api.urls')),
|
||||
|
||||
url(r'^api/user/', include('openedx.core.djangoapps.user_api.accounts.urls')),
|
||||
|
||||
url(r'^notifier_api/', include('notifier_api.urls')),
|
||||
|
||||
url(r'^lang_pref/', include('lang_pref.urls')),
|
||||
|
||||
42
openedx/core/djangoapps/user_api/accounts/serializers.py
Normal file
42
openedx/core/djangoapps/user_api/accounts/serializers.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import UserProfile
|
||||
|
||||
|
||||
class AccountUserSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""
|
||||
Class that serializes the portion of User model needed for account information.
|
||||
"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ("username", "email", "date_joined")
|
||||
read_only_fields = ("username", "email", "date_joined")
|
||||
|
||||
|
||||
class AccountLegacyProfileSerializer(serializers.HyperlinkedModelSerializer):
|
||||
"""
|
||||
Class that serializes the portion of UserProfile model needed for account information.
|
||||
"""
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = (
|
||||
"name", "gender", "goals", "year_of_birth", "level_of_education", "language", "country", "mailing_address"
|
||||
)
|
||||
read_only_fields = ("name",)
|
||||
|
||||
def transform_gender(self, obj, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
def transform_country(self, obj, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
def transform_level_of_education(self, obj, value):
|
||||
""" Converts empty string to None, to indicate not set. Replaced by to_representation in version 3. """
|
||||
return AccountLegacyProfileSerializer.convert_empty_to_None(value)
|
||||
|
||||
@staticmethod
|
||||
def convert_empty_to_None(value):
|
||||
""" Helper method to convert empty string to None (other values pass through). """
|
||||
return None if value == "" else value
|
||||
248
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
Normal file
248
openedx/core/djangoapps/user_api/accounts/tests/test_views.py
Normal file
@@ -0,0 +1,248 @@
|
||||
import unittest
|
||||
import ddt
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
from django.test import TestCase
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from student.models import UserProfile
|
||||
|
||||
TEST_PASSWORD = "test"
|
||||
|
||||
@ddt.ddt
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class TestAccountAPI(APITestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAccountAPI, self).setUp()
|
||||
|
||||
self.anonymous_client = APIClient()
|
||||
|
||||
self.different_user = UserFactory.create(password=TEST_PASSWORD)
|
||||
self.different_client = APIClient()
|
||||
|
||||
self.staff_user = UserFactory(is_staff=True, password=TEST_PASSWORD)
|
||||
self.staff_client = APIClient()
|
||||
|
||||
self.user = UserFactory.create(password=TEST_PASSWORD)
|
||||
|
||||
self.url = reverse("accounts_api", kwargs={'username': self.user.username})
|
||||
|
||||
def test_get_account_anonymous_user(self):
|
||||
"""
|
||||
Test that an anonymous client (not logged in) cannot call get.
|
||||
"""
|
||||
self.send_get(self.anonymous_client, expected_status=401)
|
||||
|
||||
def test_get_account_different_user(self):
|
||||
"""
|
||||
Test that a client (logged in) cannot get the account information for a different client.
|
||||
"""
|
||||
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
|
||||
self.send_get(self.different_client, expected_status=404)
|
||||
|
||||
def test_get_account_default(self):
|
||||
"""
|
||||
Test that a client (logged in) can get her own account information (using default legacy profile information,
|
||||
as created by the test UserFactory).
|
||||
"""
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_get(self.client)
|
||||
data = response.data
|
||||
self.assertEqual(11, len(data))
|
||||
self.assertEqual(self.user.username, data["username"])
|
||||
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
|
||||
for empty_field in ("year_of_birth", "level_of_education", "mailing_address"):
|
||||
self.assertIsNone(data[empty_field])
|
||||
self.assertIsNone(data["country"])
|
||||
# TODO: what should the format of this be?
|
||||
self.assertEqual("", data["language"])
|
||||
self.assertEqual("m", data["gender"])
|
||||
self.assertEqual("World domination", data["goals"])
|
||||
self.assertEqual(self.user.email, data["email"])
|
||||
self.assertIsNotNone(data["date_joined"])
|
||||
|
||||
@ddt.data(
|
||||
("client", "user"),
|
||||
("staff_client", "staff_user"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_get_account(self, api_client, user):
|
||||
"""
|
||||
Test that a client (logged in) can get her own account information. Also verifies that a "is_staff"
|
||||
user can get the account information for other users.
|
||||
"""
|
||||
# Create some test profile values.
|
||||
legacy_profile = UserProfile.objects.get(id=self.user.id)
|
||||
legacy_profile.country = "US"
|
||||
legacy_profile.level_of_education = "m"
|
||||
legacy_profile.year_of_birth = 1900
|
||||
legacy_profile.goals = "world peace"
|
||||
legacy_profile.mailing_address = "Park Ave"
|
||||
legacy_profile.save()
|
||||
|
||||
client = self.login_client(api_client, user)
|
||||
response = self.send_get(client)
|
||||
data = response.data
|
||||
self.assertEqual(11, len(data))
|
||||
self.assertEqual(self.user.username, data["username"])
|
||||
self.assertEqual(self.user.first_name + " " + self.user.last_name, data["name"])
|
||||
self.assertEqual("US", data["country"])
|
||||
self.assertEqual("", data["language"])
|
||||
self.assertEqual("m", data["gender"])
|
||||
self.assertEqual(1900, data["year_of_birth"])
|
||||
self.assertEqual("m", data["level_of_education"])
|
||||
self.assertEqual("world peace", data["goals"])
|
||||
self.assertEqual("Park Ave", data['mailing_address'])
|
||||
self.assertEqual(self.user.email, data["email"])
|
||||
self.assertIsNotNone(data["date_joined"])
|
||||
|
||||
def test_get_account_empty_string(self):
|
||||
"""
|
||||
Test the conversion of empty strings to None for certain fields.
|
||||
"""
|
||||
legacy_profile = UserProfile.objects.get(id=self.user.id)
|
||||
legacy_profile.country = ""
|
||||
legacy_profile.level_of_education = ""
|
||||
legacy_profile.gender = ""
|
||||
legacy_profile.save()
|
||||
|
||||
self.client.login(username=self.user.username, password=TEST_PASSWORD)
|
||||
response = self.send_get(self.client)
|
||||
for empty_field in ("level_of_education", "gender", "country"):
|
||||
self.assertIsNone(response.data[empty_field])
|
||||
|
||||
@ddt.data(
|
||||
(
|
||||
"client", "user", "gender", "f", "not a gender",
|
||||
"Select a valid choice. not a gender is not one of the available choices."
|
||||
),
|
||||
(
|
||||
"client", "user", "level_of_education", "none", "x",
|
||||
"Select a valid choice. x is not one of the available choices."
|
||||
),
|
||||
("client", "user", "country", "GB", "XY", "Select a valid choice. XY is not one of the available choices."),
|
||||
("client", "user", "year_of_birth", 2009, "not_an_int", "Enter a whole number."),
|
||||
("client", "user", "language", "Creole"),
|
||||
("client", "user", "goals", "Smell the roses"),
|
||||
("client", "user", "mailing_address", "Sesame Street"),
|
||||
# All of the fields can be edited by is_staff, but iterating through all of them again seems like overkill.
|
||||
# Just test a representative field.
|
||||
("staff_client", "staff_user", "goals", "Smell the roses"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_patch_account(
|
||||
self, api_client, user, field, value, fails_validation_value=None, developer_validation_message=None
|
||||
):
|
||||
"""
|
||||
Test the behavior of patch, when using the correct content_type.
|
||||
"""
|
||||
client = self.login_client(api_client, user)
|
||||
self.send_patch(client, {field: value})
|
||||
|
||||
get_response = self.send_get(client)
|
||||
self.assertEqual(value, get_response.data[field])
|
||||
|
||||
if fails_validation_value:
|
||||
error_response = self.send_patch(client, {field: fails_validation_value}, expected_status=400)
|
||||
self.assertEqual(
|
||||
"Value '{0}' is not valid for field '{1}'.".format(fails_validation_value, field),
|
||||
error_response.data["field_errors"][field]["user_message"]
|
||||
)
|
||||
self.assertEqual(
|
||||
developer_validation_message,
|
||||
error_response.data["field_errors"][field]["developer_message"]
|
||||
)
|
||||
else:
|
||||
# If there are no values that would fail validation, then empty string should be supported.
|
||||
self.send_patch(client, {field: ""})
|
||||
|
||||
get_response = self.send_get(client)
|
||||
self.assertEqual("", get_response.data[field])
|
||||
|
||||
@ddt.data(
|
||||
("client", "user"),
|
||||
("staff_client", "staff_user"),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_patch_account_noneditable(self, api_client, user):
|
||||
"""
|
||||
Tests the behavior of patch when a read-only field is attempted to be edited.
|
||||
"""
|
||||
client = self.login_client(api_client, user)
|
||||
|
||||
def verify_error_response(field_name, data):
|
||||
self.assertEqual(
|
||||
"This field is not editable via this API", data["field_errors"][field_name]["developer_message"]
|
||||
)
|
||||
self.assertEqual(
|
||||
"Field '{0}' cannot be edited.".format(field_name), data["field_errors"][field_name]["user_message"]
|
||||
)
|
||||
|
||||
for field_name in ["username", "email", "date_joined", "name"]:
|
||||
response = self.send_patch(client, {field_name: "will_error", "gender": "f"}, expected_status=400)
|
||||
verify_error_response(field_name, response.data)
|
||||
|
||||
# Make sure that gender did not change.
|
||||
response = self.send_get(client)
|
||||
self.assertEqual("m", response.data["gender"])
|
||||
|
||||
# Test error message with multiple read-only items
|
||||
response = self.send_patch(client, {"username": "will_error", "email": "xx"}, expected_status=400)
|
||||
self.assertEqual(2, len(response.data["field_errors"]))
|
||||
verify_error_response("username", response.data)
|
||||
verify_error_response("email", response.data)
|
||||
|
||||
def test_patch_bad_content_type(self):
|
||||
"""
|
||||
Test the behavior of patch when an incorrect content_type is specified.
|
||||
"""
|
||||
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)
|
||||
|
||||
def test_patch_account_empty_string(self):
|
||||
"""
|
||||
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=TEST_PASSWORD)
|
||||
for field_name in ["gender", "level_of_education", "country"]:
|
||||
self.send_patch(self.client, {field_name: ""})
|
||||
response = self.send_get(self.client)
|
||||
# Although throwing a 400 might be reasonable, the default DRF behavior with ModelSerializer
|
||||
# is to convert to None, which also seems acceptable (and is difficult to override).
|
||||
self.assertIsNone(response.data[field_name])
|
||||
|
||||
# Verify that the behavior is the same for sending None.
|
||||
self.send_patch(self.client, {field_name: ""})
|
||||
response = self.send_get(self.client)
|
||||
self.assertIsNone(response.data[field_name])
|
||||
|
||||
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=TEST_PASSWORD)
|
||||
return client
|
||||
|
||||
def send_patch(self, client, json_data, content_type="application/merge-patch+json", expected_status=204):
|
||||
"""
|
||||
Helper method for sending a patch to the server, defaulting to application/merge-patch+json content_type.
|
||||
Verifies the expected status and returns the response.
|
||||
"""
|
||||
response = client.patch(self.url, data=json.dumps(json_data), content_type=content_type)
|
||||
self.assertEqual(expected_status, response.status_code)
|
||||
return response
|
||||
|
||||
def send_get(self, client, expected_status=200):
|
||||
"""
|
||||
Helper method for sending a GET to the server. Verifies the expected status and returns the response.
|
||||
"""
|
||||
response = client.get(self.url)
|
||||
self.assertEqual(expected_status, response.status_code)
|
||||
return response
|
||||
14
openedx/core/djangoapps/user_api/accounts/urls.py
Normal file
14
openedx/core/djangoapps/user_api/accounts/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from .views import AccountView
|
||||
|
||||
from django.conf.urls import include, patterns, url
|
||||
|
||||
USERNAME_PATTERN = r'(?P<username>[\w.+-]+)'
|
||||
|
||||
urlpatterns = patterns(
|
||||
'',
|
||||
url(
|
||||
r'^v0/accounts/' + USERNAME_PATTERN + '$',
|
||||
AccountView.as_view(),
|
||||
name="accounts_api"
|
||||
)
|
||||
)
|
||||
160
openedx/core/djangoapps/user_api/accounts/views.py
Normal file
160
openedx/core/djangoapps/user_api/accounts/views.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""
|
||||
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 django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
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 rest_framework import permissions
|
||||
from rest_framework import parsers
|
||||
|
||||
from student.models import UserProfile
|
||||
from openedx.core.djangoapps.user_api.accounts.serializers import AccountLegacyProfileSerializer, AccountUserSerializer
|
||||
from openedx.core.lib.api.permissions import IsUserInUrlOrStaff
|
||||
from openedx.core.lib.api.parsers import MergePatchParser
|
||||
|
||||
|
||||
class AccountView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
Get or update the user's account information. Updates are only supported through merge patch.
|
||||
|
||||
**Example Requests**:
|
||||
|
||||
GET /api/user/v0/accounts/{username}/
|
||||
|
||||
PATCH /api/user/v0/accounts/{username}/ with content_type "application/merge-patch+json"
|
||||
|
||||
**Response Values for GET**
|
||||
|
||||
* username: username associated with the account (not editable)
|
||||
|
||||
* name: full name of the user (not editable through this API)
|
||||
|
||||
* email: email for the user (not editable through this API)
|
||||
|
||||
* date_joined: date this account was created (not editable), in the string format provided by
|
||||
datetime (for example, "2014-08-26T17:52:11Z")
|
||||
|
||||
* gender: null (not set), "m", "f", or "o"
|
||||
|
||||
* year_of_birth: null or integer year
|
||||
|
||||
* level_of_education: null (not set), or one of the following choices:
|
||||
|
||||
* "p" signifying "Doctorate"
|
||||
* "m" signifying "Master's or professional degree"
|
||||
* "b" signifying "Bachelor's degree"
|
||||
* "a" signifying "Associate's degree"
|
||||
* "hs" signifying "Secondary/high school"
|
||||
* "jhs" signifying "Junior secondary/junior high/middle school"
|
||||
* "el" signifying "Elementary/primary school"
|
||||
* "none" signifying "None"
|
||||
* "o" signifying "Other"
|
||||
|
||||
* language: null or name of preferred language
|
||||
|
||||
* country: null (not set), or a Country corresponding to one of the ISO 3166-1 countries
|
||||
|
||||
* mailing_address: null or textual representation of mailing address
|
||||
|
||||
* goals: null or textual representation of goals
|
||||
|
||||
**Response for PATCH**
|
||||
|
||||
Returns a 204 status if successful, with no additional content.
|
||||
If "application/merge-patch+json" is not the specified content_type, returns a 415 status.
|
||||
|
||||
"""
|
||||
authentication_classes = (OAuth2Authentication, SessionAuthentication)
|
||||
permission_classes = (permissions.IsAuthenticated, IsUserInUrlOrStaff)
|
||||
parser_classes = (MergePatchParser,)
|
||||
|
||||
def get(self, request, username):
|
||||
"""
|
||||
GET /api/user/v0/accounts/{username}/
|
||||
"""
|
||||
existing_user, existing_user_profile = self._get_user_and_profile(username)
|
||||
user_serializer = AccountUserSerializer(existing_user)
|
||||
legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile)
|
||||
|
||||
return Response(dict(user_serializer.data, **legacy_profile_serializer.data))
|
||||
|
||||
def patch(self, request, username):
|
||||
"""
|
||||
PATCH /api/user/v0/accounts/{username}/
|
||||
|
||||
Note that this implementation is the "merge patch" implementation proposed in
|
||||
https://tools.ietf.org/html/rfc7396. The content_type must be "application/merge-patch+json" or
|
||||
else an error response with status code 415 will be returned.
|
||||
"""
|
||||
existing_user, existing_user_profile = self._get_user_and_profile(username)
|
||||
|
||||
# Check for fields that are not editable. Marking them read-only causes them to be ignored, but we wish to 400.
|
||||
update = request.DATA
|
||||
read_only_fields = set(update.keys()).intersection(
|
||||
AccountUserSerializer.Meta.read_only_fields + AccountLegacyProfileSerializer.Meta.read_only_fields
|
||||
)
|
||||
if read_only_fields:
|
||||
field_errors = {}
|
||||
for read_only_field in read_only_fields:
|
||||
field_errors[read_only_field] = {
|
||||
"developer_message": "This field is not editable via this API",
|
||||
"user_message": _("Field '{field_name}' cannot be edited.".format(field_name=read_only_field))
|
||||
}
|
||||
response_data = {"field_errors": field_errors}
|
||||
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
user_serializer = AccountUserSerializer(existing_user, data=update)
|
||||
legacy_profile_serializer = AccountLegacyProfileSerializer(existing_user_profile, data=update)
|
||||
|
||||
for serializer in user_serializer, legacy_profile_serializer:
|
||||
validation_errors = self._get_validation_errors(update, serializer)
|
||||
if validation_errors:
|
||||
return Response(validation_errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
serializer.save()
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
def _get_user_and_profile(self, username):
|
||||
"""
|
||||
Helper method to return the legacy user and profile objects based on username.
|
||||
"""
|
||||
try:
|
||||
existing_user = User.objects.get(username=username)
|
||||
except ObjectDoesNotExist:
|
||||
return Response({}, status=status.HTTP_404_NOT_FOUND)
|
||||
existing_user_profile = UserProfile.objects.get(user=existing_user)
|
||||
|
||||
return existing_user, existing_user_profile
|
||||
|
||||
def _get_validation_errors(self, update, serializer):
|
||||
"""
|
||||
Helper method that returns any validation errors that are present.
|
||||
"""
|
||||
validation_errors = {}
|
||||
if not serializer.is_valid():
|
||||
field_errors = {}
|
||||
errors = serializer.errors
|
||||
for key, value in errors.iteritems():
|
||||
if isinstance(value, list) and len(value) > 0:
|
||||
developer_message = value[0]
|
||||
else:
|
||||
developer_message = "Invalid value: {field_value}'".format(field_value=update[key])
|
||||
field_errors[key] = {
|
||||
"developer_message": developer_message,
|
||||
"user_message": _("Value '{field_value}' is not valid for field '{field_name}'.".format(
|
||||
field_value=update[key], field_name=key)
|
||||
)
|
||||
}
|
||||
|
||||
validation_errors['field_errors'] = field_errors
|
||||
return validation_errors
|
||||
8
openedx/core/lib/api/parsers.py
Normal file
8
openedx/core/lib/api/parsers.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from rest_framework import parsers
|
||||
|
||||
|
||||
class MergePatchParser(parsers.JSONParser):
|
||||
"""
|
||||
Custom parser to be used with the "merge patch" implementation (https://tools.ietf.org/html/rfc7396).
|
||||
"""
|
||||
media_type = 'application/merge-patch+json'
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.conf import settings
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
|
||||
|
||||
class ApiKeyHeaderPermission(permissions.BasePermission):
|
||||
@@ -31,3 +32,26 @@ class IsAuthenticatedOrDebug(permissions.BasePermission):
|
||||
|
||||
user = getattr(request, 'user', None)
|
||||
return user and user.is_authenticated()
|
||||
|
||||
|
||||
class IsUserInUrl(permissions.BasePermission):
|
||||
"""
|
||||
Permission that checks to see if the request user matches the user in the URL.
|
||||
"""
|
||||
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):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user