Commit 2cd62c7b by John Eskew

Upgrade django-rest-framework version to edX fork, which is DRF v3.6.3

with a custom patch needed by edx-platform.
Upgrade django-filter as well to v1.0.4
Import DjangoFilterBackend from the correct module - django_filter.
Add django-filter to INSTALLED_APPS.
parent 28bdfd4f
...@@ -1045,6 +1045,9 @@ INSTALLED_APPS = ( ...@@ -1045,6 +1045,9 @@ INSTALLED_APPS = (
# Waffle related utilities # Waffle related utilities
'openedx.core.djangoapps.waffle_utils', 'openedx.core.djangoapps.waffle_utils',
# DRF filters
'django_filters',
) )
......
""" """
Tests for Discussion API views Tests for Discussion API views
""" """
from __future__ import unicode_literals
import json import json
from datetime import datetime from datetime import datetime
from urlparse import urlparse from urlparse import urlparse
...@@ -376,7 +378,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro ...@@ -376,7 +378,7 @@ class ThreadViewSetListTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase, Pro
results=expected_threads, results=expected_threads,
count=1, count=1,
num_pages=2, num_pages=2,
next_link="http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&page=2", next_link="http://testserver/api/discussion/v1/threads/?course_id=x%2Fy%2Fz&following=&page=2",
previous_link=None previous_link=None
) )
expected_response.update({"text_search_rewrite": None}) expected_response.update({"text_search_rewrite": None})
......
""" """
Discussion API test utilities Discussion API test utilities
""" """
from __future__ import unicode_literals
import hashlib import hashlib
import json import json
import re import re
......
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from django_filters.rest_framework import DjangoFilterBackend
from edx_rest_framework_extensions.authentication import JwtAuthentication from edx_rest_framework_extensions.authentication import JwtAuthentication
from rest_framework import permissions, viewsets from rest_framework import permissions, viewsets
from rest_framework.decorators import list_route from rest_framework.decorators import list_route
from rest_framework.filters import DjangoFilterBackend
from rest_framework.response import Response from rest_framework.response import Response
from experiments import filters, serializers from experiments import filters, serializers
......
...@@ -499,7 +499,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView): ...@@ -499,7 +499,7 @@ class TeamsListView(ExpandableFieldViewMixin, GenericAPIView):
return Response(status=status.HTTP_403_FORBIDDEN) return Response(status=status.HTTP_403_FORBIDDEN)
data = request.data.copy() data = request.data.copy()
data['course_id'] = course_key data['course_id'] = unicode(course_key)
serializer = CourseTeamCreationSerializer(data=data) serializer = CourseTeamCreationSerializer(data=data)
add_serializer_errors(serializer, data, field_errors) add_serializer_errors(serializer, data, field_errors)
......
...@@ -2248,6 +2248,9 @@ INSTALLED_APPS = ( ...@@ -2248,6 +2248,9 @@ INSTALLED_APPS = (
'openedx.features.enterprise_support', 'openedx.features.enterprise_support',
'experiments', 'experiments',
# DRF filters
'django_filters',
) )
######################### CSRF ######################################### ######################### CSRF #########################################
......
...@@ -187,7 +187,7 @@ def update_account_settings(requesting_user, update, username=None): ...@@ -187,7 +187,7 @@ def update_account_settings(requesting_user, update, username=None):
# We have not found a way using signals to get the language proficiency changes (grouped by user). # We have not found a way using signals to get the language proficiency changes (grouped by user).
# As a workaround, store old and new values here and emit them after save is complete. # As a workaround, store old and new values here and emit them after save is complete.
if "language_proficiencies" in update: if "language_proficiencies" in update:
old_language_proficiencies = legacy_profile_serializer.data["language_proficiencies"] old_language_proficiencies = list(existing_user_profile.language_proficiencies.values('code'))
for serializer in user_serializer, legacy_profile_serializer: for serializer in user_serializer, legacy_profile_serializer:
serializer.save() serializer.save()
......
...@@ -454,7 +454,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -454,7 +454,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
("country", "GB", "XY", u'"XY" is not a valid choice.'), ("country", "GB", "XY", u'"XY" is not a valid choice.'),
("year_of_birth", 2009, "not_an_int", u"A valid integer is required."), ("year_of_birth", 2009, "not_an_int", u"A valid integer is required."),
("name", "bob", "z" * 256, u"Ensure this value has at most 255 characters (it has 256)."), ("name", "bob", "z" * 256, u"Ensure this value has at most 255 characters (it has 256)."),
("name", u"ȻħȺɍłɇs", "z ", "The name field must be at least 2 characters long."), ("name", u"ȻħȺɍłɇs", "z ", u"The name field must be at least 2 characters long."),
("goals", "Smell the roses"), ("goals", "Smell the roses"),
("mailing_address", "Sesame Street"), ("mailing_address", "Sesame Street"),
# Note that we store the raw data, so it is up to client to escape the HTML. # Note that we store the raw data, so it is up to client to escape the HTML.
...@@ -677,16 +677,25 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -677,16 +677,25 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
self.assertItemsEqual(response.data["language_proficiencies"], proficiencies) self.assertItemsEqual(response.data["language_proficiencies"], proficiencies)
@ddt.data( @ddt.data(
(u"not_a_list", {u'non_field_errors': [u'Expected a list of items but got type "unicode".']}), (
([u"not_a_JSON_object"], [{u'non_field_errors': [u'Invalid data. Expected a dictionary, but got unicode.']}]), u"not_a_list",
([{}], [OrderedDict([('code', [u'This field is required.'])])]), {u'non_field_errors': [u'Expected a list of items but got type "unicode".']}
),
(
[u"not_a_JSON_object"],
[{u'non_field_errors': [u'Invalid data. Expected a dictionary, but got unicode.']}]
),
(
[{}],
[{'code': [u'This field is required.']}]
),
( (
[{u"code": u"invalid_language_code"}], [{u"code": u"invalid_language_code"}],
[OrderedDict([('code', [u'"invalid_language_code" is not a valid choice.'])])] [{'code': [u'"invalid_language_code" is not a valid choice.']}]
), ),
( (
[{u"code": u"kw"}, {u"code": u"el"}, {u"code": u"kw"}], [{u"code": u"kw"}, {u"code": u"el"}, {u"code": u"kw"}],
['The language_proficiencies field must consist of unique languages'] [u'The language_proficiencies field must consist of unique languages']
), ),
) )
@ddt.unpack @ddt.unpack
......
...@@ -10,7 +10,7 @@ from functools import wraps ...@@ -10,7 +10,7 @@ from functools import wraps
from django import forms from django import forms
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest, HttpRequest
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
...@@ -407,26 +407,32 @@ def shim_student_view(view_func, check_logged_in=False): ...@@ -407,26 +407,32 @@ def shim_student_view(view_func, check_logged_in=False):
""" """
@wraps(view_func) @wraps(view_func)
def _inner(request): # pylint: disable=missing-docstring def _inner(request): # pylint: disable=missing-docstring
# Ensure that the POST querydict is mutable # Make a copy of the current POST request to modify.
request.POST = request.POST.copy() modified_request = request.POST.copy()
if isinstance(request, HttpRequest):
# Works for an HttpRequest but not a rest_framework.request.Request.
request.POST = modified_request
else:
# The request must be a rest_framework.request.Request.
request._data = modified_request
# The login and registration handlers in student view try to change # The login and registration handlers in student view try to change
# the user's enrollment status if these parameters are present. # the user's enrollment status if these parameters are present.
# Since we want the JavaScript client to communicate directly with # Since we want the JavaScript client to communicate directly with
# the enrollment API, we want to prevent the student views from # the enrollment API, we want to prevent the student views from
# updating enrollments. # updating enrollments.
if "enrollment_action" in request.POST: if "enrollment_action" in modified_request:
del request.POST["enrollment_action"] del modified_request["enrollment_action"]
if "course_id" in request.POST: if "course_id" in modified_request:
del request.POST["course_id"] del modified_request["course_id"]
# Include the course ID if it's specified in the analytics info # Include the course ID if it's specified in the analytics info
# so it can be included in analytics events. # so it can be included in analytics events.
if "analytics" in request.POST: if "analytics" in modified_request:
try: try:
analytics = json.loads(request.POST["analytics"]) analytics = json.loads(modified_request["analytics"])
if "enroll_course_id" in analytics: if "enroll_course_id" in analytics:
request.POST["course_id"] = analytics.get("enroll_course_id") modified_request["course_id"] = analytics.get("enroll_course_id")
except (ValueError, TypeError): except (ValueError, TypeError):
LOGGER.error( LOGGER.error(
u"Could not parse analytics object sent to user API: {analytics}".format( u"Could not parse analytics object sent to user API: {analytics}".format(
......
...@@ -113,6 +113,7 @@ def update_user_preferences(requesting_user, update, user=None): ...@@ -113,6 +113,7 @@ def update_user_preferences(requesting_user, update, user=None):
for preference_key in update.keys(): for preference_key in update.keys():
preference_value = update[preference_key] preference_value = update[preference_key]
if preference_value is not None: if preference_value is not None:
preference_value = unicode(preference_value)
try: try:
serializer = create_user_preference_serializer(user, preference_key, preference_value) serializer = create_user_preference_serializer(user, preference_key, preference_value)
validate_user_preference_serializer(serializer, preference_key, preference_value) validate_user_preference_serializer(serializer, preference_key, preference_value)
...@@ -129,6 +130,7 @@ def update_user_preferences(requesting_user, update, user=None): ...@@ -129,6 +130,7 @@ def update_user_preferences(requesting_user, update, user=None):
for preference_key in update.keys(): for preference_key in update.keys():
preference_value = update[preference_key] preference_value = update[preference_key]
if preference_value is not None: if preference_value is not None:
preference_value = unicode(preference_value)
try: try:
serializer = serializers[preference_key] serializer = serializers[preference_key]
...@@ -152,7 +154,7 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern ...@@ -152,7 +154,7 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern
requesting_user (User): The user requesting to modify account information. Only the user with username requesting_user (User): The user requesting to modify account information. Only the user with username
'username' has permissions to modify account information. 'username' has permissions to modify account information.
preference_key (str): The key for the user preference. preference_key (str): The key for the user preference.
preference_value (str): The value to be stored. Non-string values will be converted to strings. preference_value (str): The value to be stored. Non-string values are converted to strings.
username (str): Optional username specifying which account should be updated. If not specified, username (str): Optional username specifying which account should be updated. If not specified,
`requesting_user.username` is assumed. `requesting_user.username` is assumed.
...@@ -166,6 +168,8 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern ...@@ -166,6 +168,8 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern
UserAPIInternalError: the operation failed due to an unexpected error. UserAPIInternalError: the operation failed due to an unexpected error.
""" """
existing_user = _get_authorized_user(requesting_user, username) existing_user = _get_authorized_user(requesting_user, username)
if preference_value is not None:
preference_value = unicode(preference_value)
serializer = create_user_preference_serializer(existing_user, preference_key, preference_value) serializer = create_user_preference_serializer(existing_user, preference_key, preference_value)
validate_user_preference_serializer(serializer, preference_key, preference_value) validate_user_preference_serializer(serializer, preference_key, preference_value)
......
...@@ -39,13 +39,14 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): ...@@ -39,13 +39,14 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer): class UserPreferenceSerializer(serializers.HyperlinkedModelSerializer):
""" """
Serializer that generates a represenation of a UserPreference entity Serializer that generates a representation of a UserPreference entity.
""" """
user = UserSerializer() user = UserSerializer()
class Meta(object): class Meta(object):
model = UserPreference model = UserPreference
depth = 1 depth = 1
fields = ('user', 'key', 'value', 'url')
class RawUserPreferenceSerializer(serializers.ModelSerializer): class RawUserPreferenceSerializer(serializers.ModelSerializer):
...@@ -57,6 +58,7 @@ class RawUserPreferenceSerializer(serializers.ModelSerializer): ...@@ -57,6 +58,7 @@ class RawUserPreferenceSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = UserPreference model = UserPreference
depth = 1 depth = 1
fields = ('user', 'key', 'value', 'url')
class ReadOnlyFieldsSerializerMixin(object): class ReadOnlyFieldsSerializerMixin(object):
......
...@@ -359,7 +359,7 @@ class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase): ...@@ -359,7 +359,7 @@ class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase):
self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI)) self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.LIST_URI))
def test_patch_list_not_allowed(self): def test_patch_list_not_allowed(self):
raise SkipTest("Django 1.4's test client does not support patch") self.assertHttpMethodNotAllowed(self.request_with_auth("patch", self.LIST_URI))
def test_delete_list_not_allowed(self): def test_delete_list_not_allowed(self):
self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI)) self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.LIST_URI))
...@@ -450,7 +450,7 @@ class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase): ...@@ -450,7 +450,7 @@ class UserPreferenceViewSetTest(CacheIsolationTestCase, UserApiTestCase):
self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri)) self.assertHttpMethodNotAllowed(self.request_with_auth("put", self.detail_uri))
def test_patch_detail_not_allowed(self): def test_patch_detail_not_allowed(self):
raise SkipTest("Django 1.4's test client does not support patch") self.assertHttpMethodNotAllowed(self.request_with_auth("patch", self.detail_uri))
def test_delete_detail_not_allowed(self): def test_delete_detail_not_allowed(self):
self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri)) self.assertHttpMethodNotAllowed(self.request_with_auth("delete", self.detail_uri))
......
...@@ -10,10 +10,11 @@ from django.utils.decorators import method_decorator ...@@ -10,10 +10,11 @@ from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie from django.views.decorators.csrf import csrf_exempt, csrf_protect, ensure_csrf_cookie
from django_countries import countries from django_countries import countries
from django_filters.rest_framework import DjangoFilterBackend
from opaque_keys import InvalidKeyError from opaque_keys import InvalidKeyError
from opaque_keys.edx import locator from opaque_keys.edx import locator
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from rest_framework import authentication, filters, generics, status, viewsets from rest_framework import authentication, generics, status, viewsets
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from rest_framework.views import APIView from rest_framework.views import APIView
...@@ -1054,7 +1055,7 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -1054,7 +1055,7 @@ class UserPreferenceViewSet(viewsets.ReadOnlyModelViewSet):
authentication_classes = (authentication.SessionAuthentication,) authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (ApiKeyHeaderPermission,) permission_classes = (ApiKeyHeaderPermission,)
queryset = UserPreference.objects.all() queryset = UserPreference.objects.all()
filter_backends = (filters.DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filter_fields = ("key", "user") filter_fields = ("key", "user")
serializer_class = UserPreferenceSerializer serializer_class = UserPreferenceSerializer
paginate_by = 10 paginate_by = 10
......
...@@ -5,11 +5,10 @@ import logging ...@@ -5,11 +5,10 @@ import logging
import django.utils.timezone import django.utils.timezone
from oauth2_provider import models as dot_models from oauth2_provider import models as dot_models
from provider.oauth2 import models as dop_models from provider.oauth2 import models as dop_models
from rest_framework import exceptions as drf_exceptions from rest_framework.exceptions import AuthenticationFailed
from rest_framework.authentication import SessionAuthentication from rest_framework.authentication import SessionAuthentication
from rest_framework_oauth.authentication import OAuth2Authentication from rest_framework_oauth.authentication import OAuth2Authentication
from openedx.core.lib.api.exceptions import AuthenticationFailed
OAUTH2_TOKEN_ERROR = u'token_error' OAUTH2_TOKEN_ERROR = u'token_error'
OAUTH2_TOKEN_ERROR_EXPIRED = u'token_expired' OAUTH2_TOKEN_ERROR_EXPIRED = u'token_expired'
...@@ -89,27 +88,25 @@ class OAuth2AuthenticationAllowInactiveUser(OAuth2Authentication): ...@@ -89,27 +88,25 @@ class OAuth2AuthenticationAllowInactiveUser(OAuth2Authentication):
succeeds, raises an AuthenticationFailed (HTTP 401) if authentication succeeds, raises an AuthenticationFailed (HTTP 401) if authentication
fails or None if the user did not try to authenticate using an access fails or None if the user did not try to authenticate using an access
token. token.
Overrides base class implementation to return edX-style error
responses.
""" """
try: try:
return super(OAuth2AuthenticationAllowInactiveUser, self).authenticate(*args, **kwargs) return super(OAuth2AuthenticationAllowInactiveUser, self).authenticate(*args, **kwargs)
except AuthenticationFailed: except AuthenticationFailed as exc:
# AuthenticationFailed is a subclass of drf_exceptions.AuthenticationFailed, if isinstance(exc.detail, dict):
# but we don't want to post-process the exception detail for our own class. developer_message = exc.detail['developer_message']
raise error_code = exc.detail['error_code']
except drf_exceptions.AuthenticationFailed as exc:
if 'No credentials provided' in exc.detail:
error_code = OAUTH2_TOKEN_ERROR_NOT_PROVIDED
elif 'Token string should not contain spaces' in exc.detail:
error_code = OAUTH2_TOKEN_ERROR_MALFORMED
else: else:
error_code = OAUTH2_TOKEN_ERROR developer_message = exc.detail
if 'No credentials provided' in developer_message:
error_code = OAUTH2_TOKEN_ERROR_NOT_PROVIDED
elif 'Token string should not contain spaces' in developer_message:
error_code = OAUTH2_TOKEN_ERROR_MALFORMED
else:
error_code = OAUTH2_TOKEN_ERROR
raise AuthenticationFailed({ raise AuthenticationFailed({
u'error_code': error_code, u'error_code': error_code,
u'developer_message': exc.detail u'developer_message': developer_message
}) })
def authenticate_credentials(self, request, access_token): def authenticate_credentials(self, request, access_token):
......
"""
Custom exceptions, that allow details to be passed as dict values (which can be
converted to JSON, like other API responses.
"""
from rest_framework import exceptions
# TODO: Override Throttled, UnsupportedMediaType, ValidationError. These types require
# more careful handling of arguments.
class _DictAPIException(exceptions.APIException):
"""
Intermediate class to allow exceptions to pass dict detail values. Use by
subclassing this along with another subclass of `exceptions.APIException`.
"""
def __init__(self, detail):
if isinstance(detail, dict):
self.detail = detail
else:
super(_DictAPIException, self).__init__(detail)
class AuthenticationFailed(exceptions.AuthenticationFailed, _DictAPIException):
"""
Override of DRF's AuthenticationFailed exception to allow dictionary responses.
"""
pass
class MethodNotAllowed(exceptions.MethodNotAllowed, _DictAPIException):
"""
Override of DRF's MethodNotAllowed exception to allow dictionary responses.
"""
def __init__(self, method, detail=None):
if isinstance(detail, dict):
self.detail = detail
else:
super(MethodNotAllowed, self).__init__(method, detail)
class NotAcceptable(exceptions.NotAcceptable, _DictAPIException):
"""
Override of DRF's NotAcceptable exception to allow dictionary responses.
"""
def __init__(self, detail=None, available_renderers=None):
self.available_renderers = available_renderers
if isinstance(detail, dict):
self.detail = detail
else:
super(NotAcceptable, self).__init__(detail, available_renderers)
class NotAuthenticated(exceptions.NotAuthenticated, _DictAPIException):
"""
Override of DRF's NotAuthenticated exception to allow dictionary responses.
"""
pass
class NotFound(exceptions.NotFound, _DictAPIException):
"""
Override of DRF's NotFound exception to allow dictionary responses.
"""
pass
class ParseError(exceptions.ParseError, _DictAPIException):
"""
Override of DRF's ParseError exception to allow dictionary responses.
"""
pass
class PermissionDenied(exceptions.PermissionDenied, _DictAPIException):
"""
Override of DRF's PermissionDenied exception to allow dictionary responses.
"""
pass
...@@ -6,67 +6,35 @@ from django.test import TestCase ...@@ -6,67 +6,35 @@ from django.test import TestCase
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
from rest_framework import exceptions as drf_exceptions from rest_framework import exceptions as drf_exceptions
from .. import exceptions
@attr(shard=2) @attr(shard=2)
@ddt.ddt @ddt.ddt
class TestDictExceptionsAllowDictDetails(TestCase): class TestDictExceptionsAllowDictDetails(TestCase):
""" """
Standard DRF exceptions coerce detail inputs to strings. We want to use Test that standard DRF exceptions can return dictionaries in error details.
dicts to allow better customization of error messages. Demonstrate that
we can provide dictionaries as exception details, and that custom
classes subclass the relevant DRF exceptions, to provide consistent
exception catching behavior.
""" """
def test_drf_errors_coerce_strings(self): def test_drf_errors_are_not_coerced_to_strings(self):
# Demonstrate the base issue we are trying to solve. # Demonstrate that dictionaries in exceptions are not coerced to strings.
exc = drf_exceptions.AuthenticationFailed({u'error_code': -1}) exc = drf_exceptions.AuthenticationFailed({u'error_code': -1})
self.assertEqual(exc.detail, u"{u'error_code': -1}") self.assertNotIsInstance(exc.detail, basestring)
@ddt.data( @ddt.data(
exceptions.AuthenticationFailed, drf_exceptions.AuthenticationFailed,
exceptions.NotAuthenticated, drf_exceptions.NotAuthenticated,
exceptions.NotFound, drf_exceptions.NotFound,
exceptions.ParseError, drf_exceptions.ParseError,
exceptions.PermissionDenied, drf_exceptions.PermissionDenied,
) )
def test_exceptions_allows_dict_detail(self, exception_class): def test_exceptions_allows_dict_detail(self, exception_class):
exc = exception_class({u'error_code': -1}) exc = exception_class({u'error_code': -1})
self.assertEqual(exc.detail, {u'error_code': -1}) self.assertEqual(exc.detail, {u'error_code': u'-1'})
def test_method_not_allowed_allows_dict_detail(self): def test_method_not_allowed_allows_dict_detail(self):
exc = exceptions.MethodNotAllowed(u'POST', {u'error_code': -1}) exc = drf_exceptions.MethodNotAllowed(u'POST', {u'error_code': -1})
self.assertEqual(exc.detail, {u'error_code': -1}) self.assertEqual(exc.detail, {u'error_code': u'-1'})
def test_not_acceptable_allows_dict_detail(self): def test_not_acceptable_allows_dict_detail(self):
exc = exceptions.NotAcceptable({u'error_code': -1}, available_renderers=['application/json']) exc = drf_exceptions.NotAcceptable({u'error_code': -1}, available_renderers=['application/json'])
self.assertEqual(exc.detail, {u'error_code': -1}) self.assertEqual(exc.detail, {u'error_code': u'-1'})
self.assertEqual(exc.available_renderers, ['application/json']) self.assertEqual(exc.available_renderers, ['application/json'])
@attr(shard=2)
@ddt.ddt
class TestDictExceptionSubclassing(TestCase):
"""
Custom exceptions should subclass standard DRF exceptions, so code that
catches the DRF exceptions also catches ours.
"""
@ddt.data(
(exceptions.AuthenticationFailed, drf_exceptions.AuthenticationFailed),
(exceptions.NotAcceptable, drf_exceptions.NotAcceptable),
(exceptions.NotAuthenticated, drf_exceptions.NotAuthenticated),
(exceptions.NotFound, drf_exceptions.NotFound),
(exceptions.ParseError, drf_exceptions.ParseError),
(exceptions.PermissionDenied, drf_exceptions.PermissionDenied),
)
@ddt.unpack
def test_exceptions_subclass_drf_exceptions(self, exception_class, drf_exception_class):
exc = exception_class({u'error_code': -1})
self.assertIsInstance(exc, drf_exception_class)
def test_method_not_allowed_subclasses_drf_exception(self):
exc = exceptions.MethodNotAllowed(u'POST', {u'error_code': -1})
self.assertIsInstance(exc, drf_exceptions.MethodNotAllowed)
...@@ -20,7 +20,7 @@ django-celery==3.2.1 ...@@ -20,7 +20,7 @@ django-celery==3.2.1
django-config-models==0.1.5 django-config-models==0.1.5
django-countries==4.0 django-countries==4.0
django-extensions==1.5.9 django-extensions==1.5.9
django-filter==0.11.0 django-filter==1.0.4
django-ipware==1.1.0 django-ipware==1.1.0
django-model-utils==2.3.1 django-model-utils==2.3.1
django-mptt==0.7.4 django-mptt==0.7.4
...@@ -35,9 +35,7 @@ django-statici18n==1.4.0 ...@@ -35,9 +35,7 @@ django-statici18n==1.4.0
django-storages==1.4.1 django-storages==1.4.1
django-method-override==0.1.0 django-method-override==0.1.0
django-user-tasks==0.1.4 django-user-tasks==0.1.4
# We need a fix to DRF 3.2.x, for now use it from our own cherry-picked repo git+https://github.com/edx/django-rest-framework.git@1ceda7c086fddffd1c440cc86856441bbf0bd9cb#egg=djangorestframework==3.6.3
#djangorestframework>=3.1,<3.2
git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3
django==1.8.18 django==1.8.18
django-waffle==0.12.0 django-waffle==0.12.0
djangorestframework-jwt==1.8.0 djangorestframework-jwt==1.8.0
......
...@@ -75,9 +75,9 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002 ...@@ -75,9 +75,9 @@ git+https://github.com/edx/lettuce.git@0.2.20.002#egg=lettuce==0.2.20.002
-e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2 -e git+https://github.com/edx/django-splash.git@v0.2#egg=django-splash==0.2
-e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock -e git+https://github.com/edx/acid-block.git@e46f9cda8a03e121a00c7e347084d142d22ebfb7#egg=acid-xblock
git+https://github.com/edx/edx-ora2.git@1.4.6#egg=ora2==1.4.6 git+https://github.com/edx/edx-ora2.git@1.4.6#egg=ora2==1.4.6
-e git+https://github.com/edx/edx-submissions.git@2.0.0#egg=edx-submissions==2.0.0 git+https://github.com/edx/edx-submissions.git@2.0.1#egg=edx-submissions==2.0.1
git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3 git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
git+https://github.com/edx/edx-val.git@0.0.17#egg=edxval==0.0.17 git+https://github.com/edx/edx-val.git@0.0.18#egg=edxval==0.0.18
git+https://github.com/edx/RecommenderXBlock.git@0e744b393cf1f8b886fe77bc697e7d9d78d65cd6#egg=recommender-xblock==1.2 git+https://github.com/edx/RecommenderXBlock.git@0e744b393cf1f8b886fe77bc697e7d9d78d65cd6#egg=recommender-xblock==1.2
git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1 git+https://github.com/solashirai/crowdsourcehinter.git@518605f0a95190949fe77bd39158450639e2e1dc#egg=crowdsourcehinter-xblock==0.1
-e git+https://github.com/edx/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock -e git+https://github.com/edx/RateXBlock.git@367e19c0f6eac8a5f002fd0f1559555f8e74bfff#egg=rate-xblock
......
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