diff --git a/.gitignore b/.gitignore
index ae73f8379..3d5f1043d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,18 +3,14 @@
*~
.*
-html/
-htmlcov/
-coverage/
-build/
-dist/
-*.egg-info/
+/site/
+/htmlcov/
+/coverage/
+/build/
+/dist/
+/*.egg-info/
+/env/
MANIFEST
-bin/
-include/
-lib/
-local/
-
!.gitignore
!.travis.yml
diff --git a/.travis.yml b/.travis.yml
index 6a4532411..3eb89dc4f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,38 +1,33 @@
language: python
-python:
- - "2.6"
- - "2.7"
- - "3.2"
- - "3.3"
+sudo: false
env:
- - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
- - DJANGO="django==1.5.1 --use-mirrors"
- - DJANGO="django==1.4.5 --use-mirrors"
- - DJANGO="django==1.3.7 --use-mirrors"
+ - TOX_ENV=py27-flake8
+ - TOX_ENV=py27-docs
+ - TOX_ENV=py34-django17
+ - TOX_ENV=py33-django17
+ - TOX_ENV=py32-django17
+ - TOX_ENV=py27-django17
+ - TOX_ENV=py34-django16
+ - TOX_ENV=py33-django16
+ - TOX_ENV=py32-django16
+ - TOX_ENV=py27-django16
+ - TOX_ENV=py26-django16
+ - TOX_ENV=py34-django15
+ - TOX_ENV=py33-django15
+ - TOX_ENV=py32-django15
+ - TOX_ENV=py27-django15
+ - TOX_ENV=py26-django15
+ - TOX_ENV=py27-django14
+ - TOX_ENV=py26-django14
+ - TOX_ENV=py34-django18beta
+ - TOX_ENV=py33-django18beta
+ - TOX_ENV=py32-django18beta
+ - TOX_ENV=py27-django18beta
install:
- - pip install $DJANGO
- - pip install defusedxml==0.3
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
- - export PYTHONPATH=.
+ - pip install tox
script:
- - python rest_framework/runtests/runtests.py
-
-matrix:
- exclude:
- - python: "3.2"
- env: DJANGO="django==1.4.5 --use-mirrors"
- - python: "3.2"
- env: DJANGO="django==1.3.7 --use-mirrors"
- - python: "3.3"
- env: DJANGO="django==1.4.5 --use-mirrors"
- - python: "3.3"
- env: DJANGO="django==1.3.7 --use-mirrors"
-
+ - tox -e $TOX_ENV
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 000000000..271fa1e35
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,9 @@
+[main]
+host = https://www.transifex.com
+
+[django-rest-framework.djangopo]
+file_filter = rest_framework/locale/Hello, world
'
return Response(data)
@@ -207,6 +153,20 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
See also: `TemplateHTMLRenderer`
+## HTMLFormRenderer
+
+Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `
:::bash', r'', output)
- output = re.sub(r'', r'', output)
- output = re.sub(r'', code_label, output)
- open(output_path, 'w').write(output.encode('utf-8'))
-
-if preview:
- import subprocess
-
- url = 'html/index.html'
-
- try:
- subprocess.Popen(["open", url]) # Mac
- except OSError:
- subprocess.Popen(["xdg-open", url]) # Linux
- except:
- os.startfile(url) # Windows
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 000000000..8aacc2dfc
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,56 @@
+site_name: Django REST framework
+site_url: http://www.django-rest-framework.org/
+site_description: Django REST framework - Web APIs for Django
+
+repo_url: https://github.com/tomchristie/django-rest-framework
+
+theme_dir: docs_theme
+
+pages:
+ - ['index.md', 'Home']
+ - ['tutorial/quickstart.md', 'Tutorial', 'Quickstart']
+ - ['tutorial/1-serialization.md', 'Tutorial', '1 - Serialization']
+ - ['tutorial/2-requests-and-responses.md', 'Tutorial', '2 - Requests and responses']
+ - ['tutorial/3-class-based-views.md', 'Tutorial', '3 - Class based views']
+ - ['tutorial/4-authentication-and-permissions.md', 'Tutorial', '4 - Authentication and permissions']
+ - ['tutorial/5-relationships-and-hyperlinked-apis.md', 'Tutorial', '5 - Relationships and hyperlinked APIs']
+ - ['tutorial/6-viewsets-and-routers.md', 'Tutorial', '6 - Viewsets and routers']
+ - ['api-guide/requests.md', 'API Guide', 'Requests']
+ - ['api-guide/responses.md', 'API Guide', 'Responses']
+ - ['api-guide/views.md', 'API Guide', 'Views']
+ - ['api-guide/generic-views.md', 'API Guide', 'Generic views']
+ - ['api-guide/viewsets.md', 'API Guide', 'Viewsets']
+ - ['api-guide/routers.md', 'API Guide', 'Routers']
+ - ['api-guide/parsers.md', 'API Guide', 'Parsers']
+ - ['api-guide/renderers.md', 'API Guide', 'Renderers']
+ - ['api-guide/serializers.md', 'API Guide', 'Serializers']
+ - ['api-guide/fields.md', 'API Guide', 'Serializer fields']
+ - ['api-guide/relations.md', 'API Guide', 'Serializer relations']
+ - ['api-guide/validators.md', 'API Guide', 'Validators']
+ - ['api-guide/authentication.md', 'API Guide', 'Authentication']
+ - ['api-guide/permissions.md', 'API Guide', 'Permissions']
+ - ['api-guide/throttling.md', 'API Guide', 'Throttling']
+ - ['api-guide/filtering.md', 'API Guide', 'Filtering']
+ - ['api-guide/pagination.md', 'API Guide', 'Pagination']
+ - ['api-guide/versioning.md', 'API Guide', 'Versioning']
+ - ['api-guide/content-negotiation.md', 'API Guide', 'Content negotiation']
+ - ['api-guide/metadata.md', 'API Guide', 'Metadata']
+ - ['api-guide/format-suffixes.md', 'API Guide', 'Format suffixes']
+ - ['api-guide/reverse.md', 'API Guide', 'Returning URLs']
+ - ['api-guide/exceptions.md', 'API Guide', 'Exceptions']
+ - ['api-guide/status-codes.md', 'API Guide', 'Status codes']
+ - ['api-guide/testing.md', 'API Guide', 'Testing']
+ - ['api-guide/settings.md', 'API Guide', 'Settings']
+ - ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
+ - ['topics/internationalization.md', 'Topics', 'Internationalization']
+ - ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
+ - ['topics/browser-enhancements.md', 'Topics',]
+ - ['topics/browsable-api.md', 'Topics', 'The Browsable API']
+ - ['topics/rest-hypermedia-hateoas.md', 'Topics', 'REST, Hypermedia & HATEOAS']
+ - ['topics/third-party-resources.md', 'Topics', 'Third Party Resources']
+ - ['topics/contributing.md', 'Topics', 'Contributing to REST framework']
+ - ['topics/project-management.md', 'Topics', 'Project management']
+ - ['topics/3.0-announcement.md', 'Topics', '3.0 Announcement']
+ - ['topics/3.1-announcement.md', 'Topics', '3.1 Announcement']
+ - ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement']
+ - ['topics/release-notes.md', 'Topics', 'Release Notes']
diff --git a/optionals.txt b/optionals.txt
deleted file mode 100644
index 4ebfceab4..000000000
--- a/optionals.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-markdown>=2.1.0
-PyYAML>=3.10
-defusedxml>=0.3
-django-filter>=0.5.4
-django-oauth-plus>=2.0
-oauth2>=1.5.211
-django-oauth2-provider>=0.2.4
diff --git a/requirements.txt b/requirements.txt
index 730c1d07a..4ec84f684 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,13 @@
-Django>=1.3
+# The base set of requirements for REST framework is actually
+# just Django, but for the purposes of development and testing
+# there are a number of packages that it is useful to install.
+
+# Laying these out as seperate requirements files, allows us to
+# only included the relevent sets when running tox, and ensures
+# we are only ever declaring out dependancies in one place.
+
+-r requirements/requirements-optionals.txt
+-r requirements/requirements-testing.txt
+-r requirements/requirements-documentation.txt
+-r requirements/requirements-codestyle.txt
+-r requirements/requirements-packaging.txt
diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt
new file mode 100644
index 000000000..4e2be24c3
--- /dev/null
+++ b/requirements/requirements-codestyle.txt
@@ -0,0 +1,3 @@
+# PEP8 code linting, which we run on all commits.
+flake8==2.3.0
+pep8==1.6.2
diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt
new file mode 100644
index 000000000..5009436e4
--- /dev/null
+++ b/requirements/requirements-documentation.txt
@@ -0,0 +1,2 @@
+# MkDocs to build our documentation.
+mkdocs==0.11.1
diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
new file mode 100644
index 000000000..af9937cfa
--- /dev/null
+++ b/requirements/requirements-optionals.txt
@@ -0,0 +1,4 @@
+# Optional packages which may be used with REST framework.
+markdown==2.5.2
+django-guardian==1.2.5
+django-filter==0.9.2
diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt
new file mode 100644
index 000000000..1efb2f836
--- /dev/null
+++ b/requirements/requirements-packaging.txt
@@ -0,0 +1,8 @@
+# Wheel for PyPI installs.
+wheel==0.24.0
+
+# Twine for secured PyPI uploads.
+twine==1.4.0
+
+# Transifex client for managing translation resources.
+transifex-client==0.10
diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt
new file mode 100644
index 000000000..a8d5d3229
--- /dev/null
+++ b/requirements/requirements-testing.txt
@@ -0,0 +1,3 @@
+# PyTest for running the tests.
+pytest==2.6.4
+pytest-django==2.8.0
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 776618ac3..f8bbeee36 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,6 +1,20 @@
-__version__ = '2.3.6'
+"""
+______ _____ _____ _____ __
+| ___ \ ___/ ___|_ _| / _| | |
+| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
+| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
+| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
+\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
+"""
-VERSION = __version__ # synonym
+__title__ = 'Django REST framework'
+__version__ = '3.1.0'
+__author__ = 'Tom Christie'
+__license__ = 'BSD 2-Clause'
+__copyright__ = 'Copyright 2011-2015 Tom Christie'
+
+# Version synonym
+VERSION = __version__
# Header encoding (see RFC5987)
HTTP_HEADER_ENCODING = 'iso-8859-1'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index cf001a24d..f0702286c 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -3,13 +3,10 @@ Provides various authentication policies.
"""
from __future__ import unicode_literals
import base64
-
from django.contrib.auth import authenticate
-from django.core.exceptions import ImproperlyConfigured
+from django.middleware.csrf import CsrfViewMiddleware
+from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, HTTP_HEADER_ENCODING
-from rest_framework.compat import CsrfViewMiddleware
-from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, provider_now
from rest_framework.authtoken.models import Token
@@ -20,7 +17,7 @@ def get_authorization_header(request):
Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
- if type(auth) == type(''):
+ if isinstance(auth, type('')):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
@@ -69,16 +66,16 @@ class BasicAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid basic header. No credentials provided.'
+ msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid basic header. Credentials string should not contain spaces.'
+ msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError):
- msg = 'Invalid basic header. Credentials not correctly base64 encoded'
+ msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
@@ -89,8 +86,13 @@ class BasicAuthentication(BaseAuthentication):
Authenticate the userid and password against username and password.
"""
user = authenticate(username=userid, password=password)
- if user is None or not user.is_active:
- raise exceptions.AuthenticationFailed('Invalid username/password')
+
+ if user is None:
+ raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
+
+ if not user.is_active:
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
+
return (user, None)
def authenticate_header(self, request):
@@ -128,7 +130,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
- raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
+ raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
@@ -156,193 +158,24 @@ class TokenAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid token header. No credentials provided.'
+ msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid token header. Token string should not contain spaces.'
+ msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
def authenticate_credentials(self, key):
try:
- token = self.model.objects.get(key=key)
+ token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
+ raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
- raise exceptions.AuthenticationFailed('User inactive or deleted')
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
def authenticate_header(self, request):
return 'Token'
-
-
-class OAuthAuthentication(BaseAuthentication):
- """
- OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`.
-
- Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- We import it from the `compat` module as `oauth`.
- """
- www_authenticate_realm = 'api'
-
- def __init__(self, *args, **kwargs):
- super(OAuthAuthentication, self).__init__(*args, **kwargs)
-
- if oauth is None:
- raise ImproperlyConfigured(
- "The 'oauth2' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- if oauth_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth-plus' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
- try:
- oauth_request = oauth_provider.utils.get_oauth_request(request)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- if not oauth_request:
- return None
-
- oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES
-
- found = any(param for param in oauth_params if param in oauth_request)
- missing = list(param for param in oauth_params if param not in oauth_request)
-
- if not found:
- # OAuth authentication was not attempted.
- return None
-
- if missing:
- # OAuth was attempted but missing parameters.
- msg = 'Missing parameters: %s' % (', '.join(missing))
- raise exceptions.AuthenticationFailed(msg)
-
- if not self.check_nonce(request, oauth_request):
- msg = 'Nonce check failed'
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
- except oauth_provider.store.InvalidConsumerError:
- msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key')
- raise exceptions.AuthenticationFailed(msg)
-
- if consumer.status != oauth_provider.consts.ACCEPTED:
- msg = 'Invalid consumer key status: %s' % consumer.get_status_display()
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- token_param = oauth_request.get_parameter('oauth_token')
- token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param)
- except oauth_provider.store.InvalidTokenError:
- msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- self.validate_token(request, consumer, token)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
- raise exceptions.AuthenticationFailed(msg)
-
- return (token.user, token)
-
- def authenticate_header(self, request):
- """
- If permission is denied, return a '401 Unauthorized' response,
- with an appropraite 'WWW-Authenticate' header.
- """
- return 'OAuth realm="%s"' % self.www_authenticate_realm
-
- def validate_token(self, request, consumer, token):
- """
- Check the token and raise an `oauth.Error` exception if invalid.
- """
- oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request)
- oauth_server.verify_request(oauth_request, consumer, token)
-
- def check_nonce(self, request, oauth_request):
- """
- Checks nonce of request, and return True if valid.
- """
- return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce'])
-
-
-class OAuth2Authentication(BaseAuthentication):
- """
- OAuth 2 authentication backend using `django-oauth2-provider`
- """
- www_authenticate_realm = 'api'
-
- def __init__(self, *args, **kwargs):
- super(OAuth2Authentication, self).__init__(*args, **kwargs)
-
- if oauth2_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth2-provider' package could not be imported. "
- "It is required for use with the 'OAuth2Authentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
-
- auth = get_authorization_header(request).split()
-
- if not auth or auth[0].lower() != b'bearer':
- return None
-
- if len(auth) == 1:
- msg = 'Invalid bearer header. No credentials provided.'
- raise exceptions.AuthenticationFailed(msg)
- elif len(auth) > 2:
- msg = 'Invalid bearer header. Token string should not contain spaces.'
- raise exceptions.AuthenticationFailed(msg)
-
- return self.authenticate_credentials(request, auth[1])
-
- def authenticate_credentials(self, request, access_token):
- """
- Authenticate the request, given the access token.
- """
-
- try:
- token = oauth2_provider.models.AccessToken.objects.select_related('user')
- # provider_now switches to timezone aware datetime when
- # the oauth2_provider version supports to it.
- token = token.get(token=access_token, expires__gt=provider_now())
- except oauth2_provider.models.AccessToken.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
- raise exceptions.AuthenticationFailed(msg)
-
- return (user, token)
-
- def authenticate_header(self, request):
- """
- Bearer is the only finalized type currently
-
- Check details on the `OAuth2Authentication.authenticate` method
- """
- return 'Bearer realm="%s"' % self.www_authenticate_realm
diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py
index d5965e404..769f62029 100644
--- a/rest_framework/authtoken/migrations/0001_initial.py
+++ b/rest_framework/authtoken/migrations/0001_initial.py
@@ -1,67 +1,26 @@
# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
+from __future__ import unicode_literals
-from rest_framework.settings import api_settings
+from django.db import models, migrations
+from django.conf import settings
-try:
- from django.contrib.auth import get_user_model
-except ImportError: # django < 1.5
- from django.contrib.auth.models import User
-else:
- User = get_user_model()
+class Migration(migrations.Migration):
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
- # Adding model 'Token'
- db.create_table('authtoken_token', (
- ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
- ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
- ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
- ))
- db.send_create_signal('authtoken', ['Token'])
-
-
- def backwards(self, orm):
- # Deleting model 'Token'
- db.delete_table('authtoken_token')
-
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- "%s.%s" % (User._meta.app_label, User._meta.module_name): {
- 'Meta': {'object_name': User._meta.module_name},
- },
- 'authtoken.token': {
- 'Meta': {'object_name': 'Token'},
- 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
- 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
- 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- }
- }
-
- complete_apps = ['authtoken']
+ operations = [
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ ('key', models.CharField(primary_key=True, serialize=False, max_length=40)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='auth_token')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 7601f5b79..a1a9315fa 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,11 +1,19 @@
-import uuid
-import hmac
-from hashlib import sha1
-from rest_framework.compat import AUTH_USER_MODEL
+import binascii
+import os
+
from django.conf import settings
from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
+# Note that we don't perform this code in the compat module due to
+# bug report #1297
+# See: https://github.com/tomchristie/django-rest-framework/issues/1297
+AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+
+
+@python_2_unicode_compatible
class Token(models.Model):
"""
The default authorization token model.
@@ -28,8 +36,7 @@ class Token(models.Model):
return super(Token, self).save(*args, **kwargs)
def generate_key(self):
- unique = uuid.uuid4()
- return hmac.new(unique.bytes, digestmod=sha1).hexdigest()
+ return binascii.hexlify(os.urandom(20)).decode()
- def __unicode__(self):
+ def __str__(self):
return self.key
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 60a3740e7..37ade255d 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -1,5 +1,7 @@
from django.contrib.auth import authenticate
-from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+
+from rest_framework import exceptions, serializers
class AuthTokenSerializer(serializers.Serializer):
@@ -15,10 +17,14 @@ class AuthTokenSerializer(serializers.Serializer):
if user:
if not user.is_active:
- raise serializers.ValidationError('User account is disabled.')
- attrs['user'] = user
- return attrs
+ msg = _('User account is disabled.')
+ raise exceptions.ValidationError(msg)
else:
- raise serializers.ValidationError('Unable to login with provided credentials.')
+ msg = _('Unable to log in with provided credentials.')
+ raise exceptions.ValidationError(msg)
else:
- raise serializers.ValidationError('Must include "username" and "password"')
+ msg = _('Must include "username" and "password".')
+ raise exceptions.ValidationError(msg)
+
+ attrs['user'] = user
+ return attrs
diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py
new file mode 100644
index 000000000..5b927f3e5
--- /dev/null
+++ b/rest_framework/authtoken/south_migrations/0001_initial.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from south.v2 import SchemaMigration
+
+try:
+ from django.contrib.auth import get_user_model
+except ImportError: # django < 1.5
+ from django.contrib.auth.models import User
+else:
+ User = get_user_model()
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Token'
+ db.create_table('authtoken_token', (
+ ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('authtoken', ['Token'])
+
+ def backwards(self, orm):
+ # Deleting model 'Token'
+ db.delete_table('authtoken_token')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ "%s.%s" % (User._meta.app_label, User._meta.module_name): {
+ 'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)},
+ },
+ 'authtoken.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['authtoken']
diff --git a/rest_framework/runtests/__init__.py b/rest_framework/authtoken/south_migrations/__init__.py
similarity index 100%
rename from rest_framework/runtests/__init__.py
rename to rest_framework/authtoken/south_migrations/__init__.py
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 7c03cb766..b75c2e252 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -1,5 +1,4 @@
from rest_framework.views import APIView
-from rest_framework import status
from rest_framework import parsers
from rest_framework import renderers
from rest_framework.response import Response
@@ -12,15 +11,13 @@ class ObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
- serializer_class = AuthTokenSerializer
- model = Token
def post(self, request):
- serializer = self.serializer_class(data=request.DATA)
- if serializer.is_valid():
- token, created = Token.objects.get_or_create(user=serializer.object['user'])
- return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer = AuthTokenSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ user = serializer.validated_data['user']
+ token, created = Token.objects.get_or_create(user=user)
+ return Response({'token': token.key})
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 6f7447add..c6a4a8698 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -5,34 +5,58 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
-
-import django
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
+from django.utils.encoding import force_text
+from django.utils.six.moves.urllib.parse import urlparse as _urlparse
+from django.utils import six
+import django
+import inspect
+try:
+ import importlib
+except ImportError:
+ from django.utils import importlib
-# Try to import six from Django, fallback to included `six`.
-try:
- from django.utils import six
-except ImportError:
- from rest_framework import six
+def unicode_repr(instance):
+ # Get the repr of an instance, but ensure it is a unicode string
+ # on both python 3 (already the case) and 2 (not the case).
+ if six.PY2:
+ return repr(instance).decode('utf-8')
+ return repr(instance)
-# location of patterns, url, include changes in 1.4 onwards
-try:
- from django.conf.urls import patterns, url, include
-except ImportError:
- from django.conf.urls.defaults import patterns, url, include
-# Handle django.utils.encoding rename:
-# smart_unicode -> smart_text
-# force_unicode -> force_text
+def unicode_to_repr(value):
+ # Coerce a unicode string to the correct repr return type, depending on
+ # the Python version. We wrap all our `__repr__` implementations with
+ # this and then use unicode throughout internally.
+ if six.PY2:
+ return value.encode('utf-8')
+ return value
+
+
+def unicode_http_header(value):
+ # Coerce HTTP header value to unicode.
+ if isinstance(value, six.binary_type):
+ return value.decode('iso-8859-1')
+ return value
+
+
+def total_seconds(timedelta):
+ # TimeDelta.total_seconds() is only available in Python 2.7
+ if hasattr(timedelta, 'total_seconds'):
+ return timedelta.total_seconds()
+ else:
+ return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0)
+
+
+# OrderedDict only available in Python 2.7.
+# This will always be the case in Django 1.7 and above, as these versions
+# no longer support Python 2.6.
+# For Django <= 1.6 and Python 2.6 fall back to SortedDict.
try:
- from django.utils.encoding import smart_text
+ from collections import OrderedDict
except ImportError:
- from django.utils.encoding import smart_unicode as smart_text
-try:
- from django.utils.encoding import force_text
-except ImportError:
- from django.utils.encoding import force_unicode as force_text
+ from django.utils.datastructures import SortedDict as OrderedDict
# HttpResponseBase only exists from 1.5 onwards
@@ -41,437 +65,163 @@ try:
except ImportError:
from django.http import HttpResponse as HttpResponseBase
+
+# contrib.postgres only supported from 1.8 onwards.
+try:
+ from django.contrib.postgres import fields as postgres_fields
+except ImportError:
+ postgres_fields = None
+
+
+# request only provides `resolver_match` from 1.5 onwards.
+def get_resolver_match(request):
+ try:
+ return request.resolver_match
+ except AttributeError:
+ # Django < 1.5
+ from django.core.urlresolvers import resolve
+ return resolve(request.path_info)
+
+
# django-filter is optional
try:
import django_filters
except ImportError:
django_filters = None
-
-# cStringIO only if it's available, otherwise StringIO
-try:
- import cStringIO.StringIO as StringIO
-except ImportError:
- StringIO = six.StringIO
-
-BytesIO = six.BytesIO
-
-
-# urlparse compat import (Required because it changed in python 3.x)
-try:
- from urllib import parse as urlparse
-except ImportError:
- import urlparse
-
-
-# Try to import PIL in either of the two ways it can end up installed.
-try:
- from PIL import Image
-except ImportError:
- try:
- import Image
- except ImportError:
- Image = None
-
-
-def get_concrete_model(model_cls):
- try:
- return model_cls._meta.concrete_model
- except AttributeError:
- # 1.3 does not include concrete model
- return model_cls
-
-
-# Django 1.5 add support for custom auth user model
-if django.VERSION >= (1, 5):
- AUTH_USER_MODEL = settings.AUTH_USER_MODEL
+if django.VERSION >= (1, 6):
+ def clean_manytomany_helptext(text):
+ return text
else:
- AUTH_USER_MODEL = 'auth.User'
+ # Up to version 1.5 many to many fields automatically suffix
+ # the `help_text` attribute with hardcoded text.
+ def clean_manytomany_helptext(text):
+ if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
+ text = text[:-69]
+ return text
+
+# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
+# Fixes (#1712). We keep the try/except for the test suite.
+guardian = None
+if 'guardian' in settings.INSTALLED_APPS:
+ try:
+ import guardian
+ import guardian.shortcuts # Fixes #1624
+ except ImportError:
+ pass
+def get_model_name(model_cls):
+ try:
+ return model_cls._meta.model_name
+ except AttributeError:
+ # < 1.6 used module_name instead of model_name
+ return model_cls._meta.module_name
+
+
+# View._allowed_methods only present from 1.5 onwards
if django.VERSION >= (1, 5):
from django.views.generic import View
else:
- from django.views.generic import View as _View
- from django.utils.decorators import classonlymethod
- from django.utils.functional import update_wrapper
+ from django.views.generic import View as DjangoView
- class View(_View):
- # 1.3 does not include head method in base View class
- # See: https://code.djangoproject.com/ticket/15668
- @classonlymethod
- def as_view(cls, **initkwargs):
- """
- Main entry point for a request-response process.
- """
- # sanitize keyword arguments
- for key in initkwargs:
- if key in cls.http_method_names:
- raise TypeError("You tried to pass in the %s method name as a "
- "keyword argument to %s(). Don't do that."
- % (key, cls.__name__))
- if not hasattr(cls, key):
- raise TypeError("%s() received an invalid keyword %r" % (
- cls.__name__, key))
-
- def view(request, *args, **kwargs):
- self = cls(**initkwargs)
- if hasattr(self, 'get') and not hasattr(self, 'head'):
- self.head = self.get
- return self.dispatch(request, *args, **kwargs)
-
- # take name and docstring from class
- update_wrapper(view, cls, updated=())
-
- # and possible attributes set by decorators
- # like csrf_exempt from dispatch
- update_wrapper(view, cls.dispatch, assigned=())
- return view
-
- # _allowed_methods only present from 1.5 onwards
+ class View(DjangoView):
def _allowed_methods(self):
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
+# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
+if django.VERSION >= (1, 8):
+ from django.core.validators import MinValueValidator, MaxValueValidator
+ from django.core.validators import MinLengthValidator, MaxLengthValidator
+else:
+ from django.core.validators import MinValueValidator as DjangoMinValueValidator
+ from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
+ from django.core.validators import MinLengthValidator as DjangoMinLengthValidator
+ from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator
+
+ class MinValueValidator(DjangoMinValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MinValueValidator, self).__init__(*args, **kwargs)
+
+ class MaxValueValidator(DjangoMaxValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MaxValueValidator, self).__init__(*args, **kwargs)
+
+ class MinLengthValidator(DjangoMinLengthValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MinLengthValidator, self).__init__(*args, **kwargs)
+
+ class MaxLengthValidator(DjangoMaxLengthValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MaxLengthValidator, self).__init__(*args, **kwargs)
+
+
+# URLValidator only accepts `message` in 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import URLValidator
+else:
+ from django.core.validators import URLValidator as DjangoURLValidator
+
+ class URLValidator(DjangoURLValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(URLValidator, self).__init__(*args, **kwargs)
+
+
+# EmailValidator requires explicit regex prior to 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import EmailValidator
+else:
+ from django.core.validators import EmailValidator as DjangoEmailValidator
+ from django.core.validators import email_re
+
+ class EmailValidator(DjangoEmailValidator):
+ def __init__(self, *args, **kwargs):
+ super(EmailValidator, self).__init__(email_re, *args, **kwargs)
+
+
# PATCH method is not implemented by Django
if 'patch' not in View.http_method_names:
View.http_method_names = View.http_method_names + ['patch']
-# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
-if django.VERSION >= (1, 4):
- from django.middleware.csrf import CsrfViewMiddleware
-else:
- import hashlib
- import re
- import random
- import logging
-
- from django.conf import settings
- from django.core.urlresolvers import get_callable
-
- try:
- from logging import NullHandler
- except ImportError:
- class NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
- logger = logging.getLogger('django.request')
-
- if not logger.handlers:
- logger.addHandler(NullHandler())
-
- def same_origin(url1, url2):
- """
- Checks if two URLs are 'same-origin'
- """
- p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
- return p1[0:2] == p2[0:2]
-
- def constant_time_compare(val1, val2):
- """
- Returns True if the two strings are equal, False otherwise.
-
- The time taken is independent of the number of characters that match.
- """
- if len(val1) != len(val2):
- return False
- result = 0
- for x, y in zip(val1, val2):
- result |= ord(x) ^ ord(y)
- return result == 0
-
- # Use the system (hardware-based) random number generator if it exists.
- if hasattr(random, 'SystemRandom'):
- randrange = random.SystemRandom().randrange
- else:
- randrange = random.randrange
-
- _MAX_CSRF_KEY = 18446744073709551616 # 2 << 63
-
- REASON_NO_REFERER = "Referer checking failed - no Referer."
- REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
- REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
- REASON_BAD_TOKEN = "CSRF token missing or incorrect."
-
- def _get_failure_view():
- """
- Returns the view to be used for CSRF rejections
- """
- return get_callable(settings.CSRF_FAILURE_VIEW)
-
- def _get_new_csrf_key():
- return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
-
- def get_token(request):
- """
- Returns the the CSRF token required for a POST form. The token is an
- alphanumeric value.
-
- A side effect of calling this function is to make the the csrf_protect
- decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
- header to the outgoing response. For this reason, you may need to use this
- function lazily, as is done by the csrf context processor.
- """
- request.META["CSRF_COOKIE_USED"] = True
- return request.META.get("CSRF_COOKIE", None)
-
- def _sanitize_token(token):
- # Allow only alphanum, and ensure we return a 'str' for the sake of the post
- # processing middleware.
- token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
- if token == "":
- # In case the cookie has been truncated to nothing at some point.
- return _get_new_csrf_key()
- else:
- return token
-
- class CsrfViewMiddleware(object):
- """
- Middleware that requires a present and correct csrfmiddlewaretoken
- for POST requests that have a CSRF cookie, and sets an outgoing
- CSRF cookie.
-
- This middleware should be used in conjunction with the csrf_token template
- tag.
- """
- # The _accept and _reject methods currently only exist for the sake of the
- # requires_csrf_token decorator.
- def _accept(self, request):
- # Avoid checking the request twice by adding a custom attribute to
- # request. This will be relevant when both decorator and middleware
- # are used.
- request.csrf_processing_done = True
- return None
-
- def _reject(self, request, reason):
- return _get_failure_view()(request, reason=reason)
-
- def process_view(self, request, callback, callback_args, callback_kwargs):
-
- if getattr(request, 'csrf_processing_done', False):
- return None
-
- try:
- csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
- # Use same token next time
- request.META['CSRF_COOKIE'] = csrf_token
- except KeyError:
- csrf_token = None
- # Generate token and store it in the request, so it's available to the view.
- request.META["CSRF_COOKIE"] = _get_new_csrf_key()
-
- # Wait until request.META["CSRF_COOKIE"] has been manipulated before
- # bailing out, so that get_token still works
- if getattr(callback, 'csrf_exempt', False):
- return None
-
- # Assume that anything not defined as 'safe' by RC2616 needs protection.
- if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
- if getattr(request, '_dont_enforce_csrf_checks', False):
- # Mechanism to turn off CSRF checks for test suite. It comes after
- # the creation of CSRF cookies, so that everything else continues to
- # work exactly the same (e.g. cookies are sent etc), but before the
- # any branches that call reject()
- return self._accept(request)
-
- if request.is_secure():
- # Suppose user visits http://example.com/
- # An active network attacker,(man-in-the-middle, MITM) sends a
- # POST form which targets https://example.com/detonate-bomb/ and
- # submits it via javascript.
- #
- # The attacker will need to provide a CSRF cookie and token, but
- # that is no problem for a MITM and the session independent
- # nonce we are using. So the MITM can circumvent the CSRF
- # protection. This is true for any HTTP connection, but anyone
- # using HTTPS expects better! For this reason, for
- # https://example.com/ we need additional protection that treats
- # http://example.com/ as completely untrusted. Under HTTPS,
- # Barth et al. found that the Referer header is missing for
- # same-domain requests in only about 0.2% of cases or less, so
- # we can use strict Referer checking.
- referer = request.META.get('HTTP_REFERER')
- if referer is None:
- logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_NO_REFERER)
-
- # Note that request.get_host() includes the port
- good_referer = 'https://%s/' % request.get_host()
- if not same_origin(referer, good_referer):
- reason = REASON_BAD_REFERER % (referer, good_referer)
- logger.warning('Forbidden (%s): %s' % (reason, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, reason)
-
- if csrf_token is None:
- # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
- # and in this way we can avoid all CSRF attacks, including login
- # CSRF.
- logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_NO_CSRF_COOKIE)
-
- # check non-cookie token for match
- request_csrf_token = ""
- if request.method == "POST":
- request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
-
- if request_csrf_token == "":
- # Fall back to X-CSRFToken, to make things easier for AJAX,
- # and possible for PUT/DELETE
- request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
-
- if not constant_time_compare(request_csrf_token, csrf_token):
- logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_BAD_TOKEN)
-
- return self._accept(request)
-
-# timezone support is new in Django 1.4
-try:
- from django.utils import timezone
-except ImportError:
- timezone = None
-
-# dateparse is ALSO new in Django 1.4
-try:
- from django.utils.dateparse import parse_date, parse_datetime, parse_time
-except ImportError:
- import datetime
- import re
-
- date_re = re.compile(
- r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})$'
- )
-
- datetime_re = re.compile(
- r'(?P\d{4})-(?P\d{1,2})-(?P\d{1,2})'
- r'[T ](?P\d{1,2}):(?P\d{1,2})'
- r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?'
- r'(?PZ|[+-]\d{1,2}:\d{1,2})?$'
- )
-
- time_re = re.compile(
- r'(?P\d{1,2}):(?P\d{1,2})'
- r'(?::(?P\d{1,2})(?:\.(?P\d{1,6})\d{0,6})?)?'
- )
-
- def parse_date(value):
- match = date_re.match(value)
- if match:
- kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
- return datetime.date(**kw)
-
- def parse_time(value):
- match = time_re.match(value)
- if match:
- kw = match.groupdict()
- if kw['microsecond']:
- kw['microsecond'] = kw['microsecond'].ljust(6, '0')
- kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
- return datetime.time(**kw)
-
- def parse_datetime(value):
- """Parse datetime, but w/o the timezone awareness in 1.4"""
- match = datetime_re.match(value)
- if match:
- kw = match.groupdict()
- if kw['microsecond']:
- kw['microsecond'] = kw['microsecond'].ljust(6, '0')
- kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
- return datetime.datetime(**kw)
-
-
-# smart_urlquote is new on Django 1.4
-try:
- from django.utils.html import smart_urlquote
-except ImportError:
- import re
- from django.utils.encoding import smart_str
- try:
- from urllib.parse import quote, urlsplit, urlunsplit
- except ImportError: # Python 2
- from urllib import quote
- from urlparse import urlsplit, urlunsplit
-
- unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
-
- def smart_urlquote(url):
- "Quotes a URL if it isn't already quoted."
- # Handle IDN before quoting.
- scheme, netloc, path, query, fragment = urlsplit(url)
- try:
- netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
- except UnicodeError: # invalid domain part
- pass
- else:
- url = urlunsplit((scheme, netloc, path, query, fragment))
-
- # An URL is considered unquoted if it contains no % characters or
- # contains a % not followed by two hexadecimal digits. See #9655.
- if '%' not in url or unquoted_percents_re.search(url):
- # See http://bugs.python.org/issue2637
- url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~')
-
- return force_text(url)
-
-
-# RequestFactory only provide `generic` from 1.5 onwards
-
+# RequestFactory only provides `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
+
try:
# In 1.5 the test client uses force_bytes
- from django.utils.encoding import force_bytes_or_smart_bytes
+ from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
except ImportError:
- # In 1.3 and 1.4 the test client just uses smart_str
+ # In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
- parsed = urlparse.urlparse(path)
+ parsed = _urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_text(parsed[4]),
- 'REQUEST_METHOD': str(method),
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': force_text(parsed[4]),
+ 'REQUEST_METHOD': six.text_type(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
- 'CONTENT_TYPE': str(content_type),
- 'wsgi.input': FakePayload(data),
- })
- elif django.VERSION <= (1, 4):
- # For 1.3 we need an empty WSGI payload
- r.update({
- 'wsgi.input': FakePayload('')
+ 'CONTENT_TYPE': six.text_type(content_type),
+ 'wsgi.input': FakePayload(data),
})
r.update(extra)
return self.request(**r)
+
# Markdown is optional
try:
import markdown
@@ -486,72 +236,17 @@ try:
safe_mode = False
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text)
-
except ImportError:
apply_markdown = None
-# Yaml is optional
-try:
- import yaml
-except ImportError:
- yaml = None
-
-
-# XML is optional
-try:
- import defusedxml.ElementTree as etree
-except ImportError:
- etree = None
-
-# OAuth is optional
-try:
- # Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- import oauth2 as oauth
-except ImportError:
- oauth = None
-
-# OAuth is optional
-try:
- import oauth_provider
- from oauth_provider.store import store as oauth_provider_store
-except (ImportError, ImproperlyConfigured):
- oauth_provider = None
- oauth_provider_store = None
-
-# OAuth 2 support is optional
-try:
- import provider.oauth2 as oauth2_provider
- from provider.oauth2 import models as oauth2_provider_models
- from provider.oauth2 import forms as oauth2_provider_forms
- from provider import scope as oauth2_provider_scope
- from provider import constants as oauth2_constants
- from provider import __version__ as provider_version
- if provider_version in ('0.2.3', '0.2.4'):
- # 0.2.3 and 0.2.4 are supported version that do not support
- # timezone aware datetimes
- import datetime
- provider_now = datetime.datetime.now
- else:
- # Any other supported version does use timezone aware datetimes
- from django.utils.timezone import now as provider_now
-except ImportError:
- oauth2_provider = None
- oauth2_provider_models = None
- oauth2_provider_forms = None
- oauth2_provider_scope = None
- oauth2_constants = None
- provider_now = None
-
-# Handle lazy strings
-from django.utils.functional import Promise
-
+# `separators` argument to `json.dumps()` differs between 2.x and 3.x
+# See: http://bugs.python.org/issue22767
if six.PY3:
- def is_non_str_iterable(obj):
- if (isinstance(obj, str) or
- (isinstance(obj, Promise) and obj._delegate_text)):
- return False
- return hasattr(obj, '__iter__')
+ SHORT_SEPARATORS = (',', ':')
+ LONG_SEPARATORS = (', ', ': ')
+ INDENT_SEPARATORS = (',', ': ')
else:
- def is_non_str_iterable(obj):
- return hasattr(obj, '__iter__')
+ SHORT_SEPARATORS = (b',', b':')
+ LONG_SEPARATORS = (b', ', b': ')
+ INDENT_SEPARATORS = (b',', b': ')
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index c69756a43..21de1acf4 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -3,21 +3,22 @@ The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function
-based views, as well as the `@action` and `@link` decorators, which are
+based views, as well as the `@detail_route` and `@list_route` decorators, which are
used to annotate methods on viewsets that should be included by routers.
"""
from __future__ import unicode_literals
-from rest_framework.compat import six
+from django.utils import six
from rest_framework.views import APIView
import types
-def api_view(http_method_names):
+def api_view(http_method_names=None):
"""
Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument.
"""
+ http_method_names = ['GET'] if (http_method_names is None) else http_method_names
def decorator(func):
@@ -107,23 +108,29 @@ def permission_classes(permission_classes):
return decorator
-def link(**kwargs):
+def detail_route(methods=None, **kwargs):
"""
- Used to mark a method on a ViewSet that should be routed for GET requests.
+ Used to mark a method on a ViewSet that should be routed for detail requests.
"""
- def decorator(func):
- func.bind_to_methods = ['get']
- func.kwargs = kwargs
- return func
- return decorator
+ methods = ['get'] if (methods is None) else methods
-
-def action(methods=['post'], **kwargs):
- """
- Used to mark a method on a ViewSet that should be routed for POST requests.
- """
def decorator(func):
func.bind_to_methods = methods
+ func.detail = True
+ func.kwargs = kwargs
+ return func
+ return decorator
+
+
+def list_route(methods=None, **kwargs):
+ """
+ Used to mark a method on a ViewSet that should be routed for list requests.
+ """
+ methods = ['get'] if (methods is None) else methods
+
+ def decorator(func):
+ func.bind_to_methods = methods
+ func.detail = False
func.kwargs = kwargs
return func
return decorator
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 425a72149..f954c13e5 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,84 +5,148 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+from django.utils import six
+from django.utils.encoding import force_text
+from django.utils.translation import ugettext_lazy as _, ungettext
from rest_framework import status
+import math
+
+
+def _force_text_recursive(data):
+ """
+ Descend into a nested data structure, forcing any
+ lazy translation strings into plain text.
+ """
+ if isinstance(data, list):
+ return [
+ _force_text_recursive(item) for item in data
+ ]
+ elif isinstance(data, dict):
+ return dict([
+ (key, _force_text_recursive(value))
+ for key, value in data.items()
+ ])
+ return force_text(data)
class APIException(Exception):
"""
Base class for REST framework exceptions.
- Subclasses should provide `.status_code` and `.detail` properties.
+ Subclasses should provide `.status_code` and `.default_detail` properties.
"""
- pass
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
+ default_detail = _('A server error occurred.')
+
+ def __init__(self, detail=None):
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
+
+ def __str__(self):
+ return self.detail
+
+
+# The recommended style for using `ValidationError` is to keep it namespaced
+# under `serializers`, in order to minimize potential confusion with Django's
+# built in `ValidationError`. For example:
+#
+# from rest_framework import serializers
+# raise serializers.ValidationError('Value was invalid')
+
+class ValidationError(APIException):
+ status_code = status.HTTP_400_BAD_REQUEST
+
+ def __init__(self, detail):
+ # For validation errors the 'detail' key is always required.
+ # The details should always be coerced to a list if not already.
+ if not isinstance(detail, dict) and not isinstance(detail, list):
+ detail = [detail]
+ self.detail = _force_text_recursive(detail)
+
+ def __str__(self):
+ return six.text_type(self.detail)
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = 'Malformed request.'
-
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ default_detail = _('Malformed request.')
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Incorrect authentication credentials.'
-
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Authentication credentials were not provided.'
-
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = 'You do not have permission to perform this action.'
+ default_detail = _('You do not have permission to perform this action.')
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+
+class NotFound(APIException):
+ status_code = status.HTTP_404_NOT_FOUND
+ default_detail = _('Not found.')
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = "Method '%s' not allowed."
+ default_detail = _('Method "{method}" not allowed.')
def __init__(self, method, detail=None):
- self.detail = (detail or self.default_detail) % method
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail).format(method=method)
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = "Could not satisfy the request's Accept header"
+ default_detail = _('Could not satisfy the request Accept header.')
def __init__(self, detail=None, available_renderers=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = "Unsupported media type '%s' in request."
+ default_detail = _('Unsupported media type "{media_type}" in request.')
def __init__(self, media_type, detail=None):
- self.detail = (detail or self.default_detail) % media_type
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail).format(
+ media_type=media_type
+ )
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = "Request was throttled."
- extra_detail = "Expected available in %d second%s."
+ default_detail = _('Request was throttled.')
+ extra_detail_singular = 'Expected available in {wait} second.'
+ extra_detail_plural = 'Expected available in {wait} seconds.'
def __init__(self, wait=None, detail=None):
- import math
- self.wait = wait and math.ceil(wait) or None
- if wait is not None:
- format = detail or self.default_detail + self.extra_detail
- self.detail = format % (self.wait, self.wait != 1 and 's' or '')
+ if detail is not None:
+ self.detail = force_text(detail)
else:
- self.detail = detail or self.default_detail
+ self.detail = force_text(self.default_detail)
+
+ if wait is None:
+ self.wait = None
+ else:
+ self.wait = math.ceil(wait)
+ self.detail += ' ' + force_text(ungettext(
+ self.extra_detail_singular.format(wait=self.wait),
+ self.extra_detail_plural.format(wait=self.wait),
+ self.wait
+ ))
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f99318877..a80862e8c 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,31 +1,39 @@
-"""
-Serializer fields perform validation on incoming data.
-
-They are very similar to Django's form fields.
-"""
from __future__ import unicode_literals
-
-import copy
-import datetime
-import inspect
-import re
-import warnings
-from decimal import Decimal, DecimalException
-from django import forms
-from django.core import validators
-from django.core.exceptions import ValidationError
from django.conf import settings
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.forms import widgets
-from django.utils.encoding import is_protected_type
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ValidationError as DjangoValidationError
+from django.core.validators import RegexValidator
+from django.forms import ImageField as DjangoImageField
+from django.utils import six, timezone
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
+from django.utils.encoding import is_protected_type, smart_text
from django.utils.translation import ugettext_lazy as _
-from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601
from rest_framework.compat import (
- timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text,
- force_text, is_non_str_iterable
+ EmailValidator, MinValueValidator, MaxValueValidator,
+ MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict,
+ unicode_repr, unicode_to_repr
)
+from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
+from rest_framework.utils import html, representation, humanize_datetime
+import collections
+import copy
+import datetime
+import decimal
+import inspect
+import re
+import uuid
+
+
+class empty:
+ """
+ This class is used to represent no data being provided for a given input
+ or output value.
+
+ It is required because `None` may be a valid input or output value.
+ """
+ pass
def is_simple_callable(obj):
@@ -44,538 +52,854 @@ def is_simple_callable(obj):
return len_args <= len_defaults
-def get_component(obj, attr_name):
+def get_attribute(instance, attrs):
"""
- Given an object, and an attribute name,
- return that attribute on the object.
+ Similar to Python's built in `getattr(instance, attr)`,
+ but takes a list of nested attributes, instead of a single attribute.
+
+ Also accepts either attribute lookup on objects or dictionary lookups.
"""
- if isinstance(obj, dict):
- val = obj.get(attr_name)
- else:
- val = getattr(obj, attr_name)
+ for attr in attrs:
+ if instance is None:
+ # Break out early if we get `None` at any point in a nested lookup.
+ return None
+ try:
+ if isinstance(instance, collections.Mapping):
+ instance = instance[attr]
+ else:
+ instance = getattr(instance, attr)
+ except ObjectDoesNotExist:
+ return None
+ if is_simple_callable(instance):
+ try:
+ instance = instance()
+ except (AttributeError, KeyError) as exc:
+ # If we raised an Attribute or KeyError here it'd get treated
+ # as an omitted field in `Field.get_attribute()`. Instead we
+ # raise a ValueError to ensure the exception is not masked.
+ raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc))
- if is_simple_callable(val):
- return val()
- return val
+ return instance
-def readable_datetime_formats(formats):
- format = ', '.join(formats).replace(ISO_8601,
- 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
- return humanize_strptime(format)
-
-
-def readable_date_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
- return humanize_strptime(format)
-
-
-def readable_time_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
- return humanize_strptime(format)
-
-
-def humanize_strptime(format_string):
- # Note that we're missing some of the locale specific mappings that
- # don't really make sense.
- mapping = {
- "%Y": "YYYY",
- "%y": "YY",
- "%m": "MM",
- "%b": "[Jan-Dec]",
- "%B": "[January-December]",
- "%d": "DD",
- "%H": "hh",
- "%I": "hh", # Requires '%p' to differentiate from '%H'.
- "%M": "mm",
- "%S": "ss",
- "%f": "uuuuuu",
- "%a": "[Mon-Sun]",
- "%A": "[Monday-Sunday]",
- "%p": "[AM|PM]",
- "%z": "[+HHMM|-HHMM]"
- }
- for key, val in mapping.items():
- format_string = format_string.replace(key, val)
- return format_string
-
-
-def strip_multiple_choice_msg(help_text):
+def set_value(dictionary, keys, value):
"""
- Remove the 'Hold down "control" ...' message that is Django enforces in
- select multiple fields on ModelForms. (Required for 1.5 and earlier)
+ Similar to Python's built in `dictionary[key] = value`,
+ but takes a list of nested keys instead of a single key.
- See https://code.djangoproject.com/ticket/9321
+ set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
+ set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
+ set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
"""
- multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
- multiple_choice_msg = force_text(multiple_choice_msg)
+ if not keys:
+ dictionary.update(value)
+ return
- return help_text.replace(multiple_choice_msg, '')
+ for key in keys[:-1]:
+ if key not in dictionary:
+ dictionary[key] = {}
+ dictionary = dictionary[key]
+
+ dictionary[keys[-1]] = value
+
+
+class CreateOnlyDefault(object):
+ """
+ This class may be used to provide default values that are only used
+ for create operations, but that do not return any value for update
+ operations.
+ """
+ def __init__(self, default):
+ self.default = default
+
+ def set_context(self, serializer_field):
+ self.is_update = serializer_field.parent.instance is not None
+ if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update:
+ self.default.set_context(serializer_field)
+
+ def __call__(self):
+ if self.is_update:
+ raise SkipField()
+ if callable(self.default):
+ return self.default()
+ return self.default
+
+ def __repr__(self):
+ return unicode_to_repr(
+ '%s(%s)' % (self.__class__.__name__, unicode_repr(self.default))
+ )
+
+
+class CurrentUserDefault(object):
+ def set_context(self, serializer_field):
+ self.user = serializer_field.context['request'].user
+
+ def __call__(self):
+ return self.user
+
+ def __repr__(self):
+ return unicode_to_repr('%s()' % self.__class__.__name__)
+
+
+class SkipField(Exception):
+ pass
+
+
+NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
+NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
+NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
+USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'
+MISSING_ERROR_MESSAGE = (
+ 'ValidationError raised by `{class_name}`, but error key `{key}` does '
+ 'not exist in the `error_messages` dictionary.'
+)
class Field(object):
- read_only = True
- creation_counter = 0
- empty = ''
- type_name = None
- partial = False
- use_files = False
- form_field_class = forms.CharField
- type_label = 'field'
+ _creation_counter = 0
- def __init__(self, source=None, label=None, help_text=None):
- self.parent = None
-
- self.creation_counter = Field.creation_counter
- Field.creation_counter += 1
-
- self.source = source
-
- if label is not None:
- self.label = smart_text(label)
-
- if help_text is not None:
- self.help_text = strip_multiple_choice_msg(smart_text(help_text))
-
- def initialize(self, parent, field_name):
- """
- Called to set up a field prior to field_to_native or field_from_native.
-
- parent - The parent serializer.
- model_field - The model field this field corresponds to, if one exists.
- """
- self.parent = parent
- self.root = parent.root or parent
- self.context = self.root.context
- self.partial = self.root.partial
- if self.partial:
- self.required = False
-
- def field_from_native(self, data, files, field_name, into):
- """
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
- """
- return
-
- def field_to_native(self, obj, field_name):
- """
- Given and object and a field name, returns the value that should be
- serialized for that field.
- """
- if obj is None:
- return self.empty
-
- if self.source == '*':
- return self.to_native(obj)
-
- source = self.source or field_name
- value = obj
-
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
-
- return self.to_native(value)
-
- def to_native(self, value):
- """
- Converts the field's value into it's simple representation.
- """
- if is_simple_callable(value):
- value = value()
-
- if is_protected_type(value):
- return value
- elif (is_non_str_iterable(value) and
- not isinstance(value, (dict, six.string_types))):
- return [self.to_native(item) for item in value]
- elif isinstance(value, dict):
- # Make sure we preserve field ordering, if it exists
- ret = SortedDict()
- for key, val in value.items():
- ret[key] = self.to_native(val)
- return ret
- return force_text(value)
-
- def attributes(self):
- """
- Returns a dictionary of attributes to be used when serializing to xml.
- """
- if self.type_name:
- return {'type': self.type_name}
- return {}
-
- def metadata(self):
- metadata = SortedDict()
- metadata['type'] = self.type_label
- metadata['required'] = getattr(self, 'required', False)
- optional_attrs = ['read_only', 'label', 'help_text',
- 'min_length', 'max_length']
- for attr in optional_attrs:
- value = getattr(self, attr, None)
- if value is not None and value != '':
- metadata[attr] = force_text(value, strings_only=True)
- return metadata
-
-
-class WritableField(Field):
- """
- Base for read/write fields.
- """
- default_validators = []
default_error_messages = {
'required': _('This field is required.'),
- 'invalid': _('Invalid value.'),
+ 'null': _('This field may not be null.')
}
- widget = widgets.TextInput
- default = None
+ default_validators = []
+ default_empty_html = empty
+ initial = None
- def __init__(self, source=None, label=None, help_text=None,
- read_only=False, required=None,
- validators=[], error_messages=None, widget=None,
- default=None, blank=None):
+ def __init__(self, read_only=False, write_only=False,
+ required=None, default=empty, initial=empty, source=None,
+ label=None, help_text=None, style=None,
+ error_messages=None, validators=None, allow_null=False):
+ self._creation_counter = Field._creation_counter
+ Field._creation_counter += 1
- # 'blank' is to be deprecated in favor of 'required'
- if blank is not None:
- warnings.warn('The `blank` keyword argument is deprecated. '
- 'Use the `required` keyword argument instead.',
- DeprecationWarning, stacklevel=2)
- required = not(blank)
+ # If `required` is unset, then use `True` unless a default is provided.
+ if required is None:
+ required = default is empty and not read_only
- super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
+ # Some combinations of keyword arguments do not make sense.
+ assert not (read_only and write_only), NOT_READ_ONLY_WRITE_ONLY
+ assert not (read_only and required), NOT_READ_ONLY_REQUIRED
+ assert not (required and default is not empty), NOT_REQUIRED_DEFAULT
+ assert not (read_only and self.__class__ == Field), USE_READONLYFIELD
self.read_only = read_only
- if required is None:
- self.required = not(read_only)
- else:
- assert not (read_only and required), "Cannot set required=True and read_only=True"
- self.required = required
+ self.write_only = write_only
+ self.required = required
+ self.default = default
+ self.source = source
+ self.initial = self.initial if (initial is empty) else initial
+ self.label = label
+ self.help_text = help_text
+ self.style = {} if style is None else style
+ self.allow_null = allow_null
+ if self.default_empty_html is not empty:
+ if not required:
+ self.default_empty_html = empty
+ elif default is not empty:
+ self.default_empty_html = default
+
+ if validators is not None:
+ self.validators = validators[:]
+
+ # These are set up by `.bind()` when the field is added to a serializer.
+ self.field_name = None
+ self.parent = None
+
+ # Collect default error message from self and parent classes
messages = {}
- for c in reversed(self.__class__.__mro__):
- messages.update(getattr(c, 'default_error_messages', {}))
+ for cls in reversed(self.__class__.__mro__):
+ messages.update(getattr(cls, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
- self.validators = self.default_validators + validators
- self.default = default if default is not None else self.default
+ def bind(self, field_name, parent):
+ """
+ Initializes the field name and parent for the field instance.
+ Called when a field is added to the parent serializer instance.
+ """
- # Widgets are ony used for HTML forms.
- widget = widget or self.widget
- if isinstance(widget, type):
- widget = widget()
- self.widget = widget
+ # In order to enforce a consistent style, we error if a redundant
+ # 'source' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ assert self.source != field_name, (
+ "It is redundant to specify `source='%s'` on field '%s' in "
+ "serializer '%s', because it is the same as the field name. "
+ "Remove the `source` keyword argument." %
+ (field_name, self.__class__.__name__, parent.__class__.__name__)
+ )
- def __deepcopy__(self, memo):
- result = copy.copy(self)
- memo[id(self)] = result
- result.validators = self.validators[:]
- return result
+ self.field_name = field_name
+ self.parent = parent
- def validate(self, value):
- if value in validators.EMPTY_VALUES and self.required:
- raise ValidationError(self.error_messages['required'])
+ # `self.label` should default to being based on the field name.
+ if self.label is None:
+ self.label = field_name.replace('_', ' ').capitalize()
+
+ # self.source should default to being the same as the field name.
+ if self.source is None:
+ self.source = field_name
+
+ # self.source_attrs is a list of attributes that need to be looked up
+ # when serializing the instance, or populating the validated data.
+ if self.source == '*':
+ self.source_attrs = []
+ else:
+ self.source_attrs = self.source.split('.')
+
+ # .validators is a lazily loaded property, that gets its default
+ # value from `get_validators`.
+ @property
+ def validators(self):
+ if not hasattr(self, '_validators'):
+ self._validators = self.get_validators()
+ return self._validators
+
+ @validators.setter
+ def validators(self, validators):
+ self._validators = validators
+
+ def get_validators(self):
+ return self.default_validators[:]
+
+ def get_initial(self):
+ """
+ Return a value to use when the field is being returned as a primitive
+ value, without any object instance.
+ """
+ return self.initial
+
+ def get_value(self, dictionary):
+ """
+ Given the *incoming* primitive data, return the value for this field
+ that should be validated and transformed to a native value.
+ """
+ if html.is_html_input(dictionary):
+ # HTML forms will represent empty fields as '', and cannot
+ # represent None or False values directly.
+ if self.field_name not in dictionary:
+ if getattr(self.root, 'partial', False):
+ return empty
+ return self.default_empty_html
+ ret = dictionary[self.field_name]
+ if ret == '' and self.allow_null:
+ # If the field is blank, and null is a valid value then
+ # determine if we should use null instead.
+ return '' if getattr(self, 'allow_blank', False) else None
+ return ret
+ return dictionary.get(self.field_name, empty)
+
+ def get_attribute(self, instance):
+ """
+ Given the *outgoing* object instance, return the primitive value
+ that should be used for this field.
+ """
+ try:
+ return get_attribute(instance, self.source_attrs)
+ except (KeyError, AttributeError) as exc:
+ if not self.required and self.default is empty:
+ raise SkipField()
+ msg = (
+ 'Got {exc_type} when attempting to get a value for field '
+ '`{field}` on serializer `{serializer}`.\nThe serializer '
+ 'field might be named incorrectly and not match '
+ 'any attribute or key on the `{instance}` instance.\n'
+ 'Original exception text was: {exc}.'.format(
+ exc_type=type(exc).__name__,
+ field=self.field_name,
+ serializer=self.parent.__class__.__name__,
+ instance=instance.__class__.__name__,
+ exc=exc
+ )
+ )
+ raise type(exc)(msg)
+
+ def get_default(self):
+ """
+ Return the default value to use when validating data if no input
+ is provided for this field.
+
+ If a default has not been set for this field then this will simply
+ return `empty`, indicating that no value should be set in the
+ validated data for this field.
+ """
+ if self.default is empty:
+ raise SkipField()
+ if callable(self.default):
+ if hasattr(self.default, 'set_context'):
+ self.default.set_context(self)
+ return self.default()
+ return self.default
+
+ def validate_empty_values(self, data):
+ """
+ Validate empty values, and either:
+
+ * Raise `ValidationError`, indicating invalid data.
+ * Raise `SkipField`, indicating that the field should be ignored.
+ * Return (True, data), indicating an empty value that should be
+ returned without any furhter validation being applied.
+ * Return (False, data), indicating a non-empty value, that should
+ have validation applied as normal.
+ """
+ if self.read_only:
+ return (True, self.get_default())
+
+ if data is empty:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
+ if self.required:
+ self.fail('required')
+ return (True, self.get_default())
+
+ if data is None:
+ if not self.allow_null:
+ self.fail('null')
+ return (True, None)
+
+ return (False, data)
+
+ def run_validation(self, data=empty):
+ """
+ Validate a simple representation and return the internal value.
+
+ The provided data may be `empty` if no representation was included
+ in the input.
+
+ May raise `SkipField` if the field should not be included in the
+ validated data.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+ value = self.to_internal_value(data)
+ self.run_validators(value)
+ return value
def run_validators(self, value):
- if value in validators.EMPTY_VALUES:
- return
+ """
+ Test the given value against all the validators on the field,
+ and either raise a `ValidationError` or simply return.
+ """
errors = []
- for v in self.validators:
+ for validator in self.validators:
+ if hasattr(validator, 'set_context'):
+ validator.set_context(self)
+
try:
- v(value)
- except ValidationError as e:
- if hasattr(e, 'code') and e.code in self.error_messages:
- message = self.error_messages[e.code]
- if e.params:
- message = message % e.params
- errors.append(message)
- else:
- errors.extend(e.messages)
+ validator(value)
+ except ValidationError as exc:
+ # If the validation error contains a mapping of fields to
+ # errors then simply raise it immediately rather than
+ # attempting to accumulate a list of errors.
+ if isinstance(exc.detail, dict):
+ raise
+ errors.extend(exc.detail)
+ except DjangoValidationError as exc:
+ errors.extend(exc.messages)
if errors:
raise ValidationError(errors)
- def field_from_native(self, data, files, field_name, into):
+ def to_internal_value(self, data):
"""
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
+ Transform the *incoming* primitive data into a native value.
"""
- if self.read_only:
- return
+ raise NotImplementedError(
+ '{cls}.to_internal_value() must be implemented.'.format(
+ cls=self.__class__.__name__
+ )
+ )
+ def to_representation(self, value):
+ """
+ Transform the *outgoing* native value into primitive data.
+ """
+ raise NotImplementedError(
+ '{cls}.to_representation() must be implemented.\n'
+ 'If you are upgrading from REST framework version 2 '
+ 'you might want `ReadOnlyField`.'.format(
+ cls=self.__class__.__name__
+ )
+ )
+
+ def fail(self, key, **kwargs):
+ """
+ A helper method that simply raises a validation error.
+ """
try:
- if self.use_files:
- files = files or {}
- native = files[field_name]
- else:
- native = data[field_name]
+ msg = self.error_messages[key]
except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- if is_simple_callable(self.default):
- native = self.default()
- else:
- native = self.default
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+ class_name = self.__class__.__name__
+ msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
+ raise AssertionError(msg)
+ message_string = msg.format(**kwargs)
+ raise ValidationError(message_string)
- value = self.from_native(native)
- if self.source == '*':
- if value:
- into.update(value)
- else:
- self.validate(value)
- self.run_validators(value)
- into[self.source or field_name] = value
-
- def from_native(self, value):
+ @property
+ def root(self):
"""
- Reverts a simple representation back to the field's value.
+ Returns the top-level serializer for this field.
"""
- return value
+ root = self
+ while root.parent is not None:
+ root = root.parent
+ return root
+
+ @property
+ def context(self):
+ """
+ Returns the context as passed to the root serializer on initialization.
+ """
+ return getattr(self.root, '_context', {})
+
+ def __new__(cls, *args, **kwargs):
+ """
+ When a field is instantiated, we store the arguments that were used,
+ so that we can present a helpful representation of the object.
+ """
+ instance = super(Field, cls).__new__(cls)
+ instance._args = args
+ instance._kwargs = kwargs
+ return instance
+
+ def __deepcopy__(self, memo):
+ """
+ When cloning fields we instantiate using the arguments it was
+ originally created with, rather than copying the complete state.
+ """
+ args = copy.deepcopy(self._args)
+ kwargs = dict(self._kwargs)
+ # Bit ugly, but we need to special case 'validators' as Django's
+ # RegexValidator does not support deepcopy.
+ # We treat validator callables as immutable objects.
+ # See https://github.com/tomchristie/django-rest-framework/issues/1954
+ validators = kwargs.pop('validators', None)
+ kwargs = copy.deepcopy(kwargs)
+ if validators is not None:
+ kwargs['validators'] = validators
+ return self.__class__(*args, **kwargs)
+
+ def __repr__(self):
+ """
+ Fields are represented using their initial calling arguments.
+ This allows us to create descriptive representations for serializer
+ instances that show all the declared fields on the serializer.
+ """
+ return unicode_to_repr(representation.field_repr(self))
-class ModelField(WritableField):
- """
- A generic field that can be used against an arbitrary model field.
- """
- def __init__(self, *args, **kwargs):
- try:
- self.model_field = kwargs.pop('model_field')
- except KeyError:
- raise ValueError("ModelField requires 'model_field' kwarg")
+# Boolean types...
- self.min_length = kwargs.pop('min_length',
- getattr(self.model_field, 'min_length', None))
- self.max_length = kwargs.pop('max_length',
- getattr(self.model_field, 'max_length', None))
- self.min_value = kwargs.pop('min_value',
- getattr(self.model_field, 'min_value', None))
- self.max_value = kwargs.pop('max_value',
- getattr(self.model_field, 'max_value', None))
-
- super(ModelField, self).__init__(*args, **kwargs)
-
- if self.min_length is not None:
- self.validators.append(validators.MinLengthValidator(self.min_length))
- if self.max_length is not None:
- self.validators.append(validators.MaxLengthValidator(self.max_length))
- if self.min_value is not None:
- self.validators.append(validators.MinValueValidator(self.min_value))
- if self.max_value is not None:
- self.validators.append(validators.MaxValueValidator(self.max_value))
-
- def from_native(self, value):
- rel = getattr(self.model_field, "rel", None)
- if rel is not None:
- return rel.to._meta.get_field(rel.field_name).to_python(value)
- else:
- return self.model_field.to_python(value)
-
- def field_to_native(self, obj, field_name):
- value = self.model_field._get_val_from_obj(obj)
- if is_protected_type(value):
- return value
- return self.model_field.value_to_string(obj)
-
- def attributes(self):
- return {
- "type": self.model_field.get_internal_type()
- }
-
-
-##### Typed Fields #####
-
-class BooleanField(WritableField):
- type_name = 'BooleanField'
- type_label = 'boolean'
- form_field_class = forms.BooleanField
- widget = widgets.CheckboxInput
+class BooleanField(Field):
default_error_messages = {
- 'invalid': _("'%s' value must be either True or False."),
+ 'invalid': _('"{input}" is not a valid boolean.')
}
- empty = False
+ default_empty_html = False
+ initial = False
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
- # Note: we set default to `False` in order to fill in missing value not
- # supplied by html form. TODO: Fix so that only html form input gets
- # this behavior.
- default = False
+ def __init__(self, **kwargs):
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
+ super(BooleanField, self).__init__(**kwargs)
- def from_native(self, value):
- if value in ('true', 't', 'True', '1'):
+ def to_internal_value(self, data):
+ if data in self.TRUE_VALUES:
return True
- if value in ('false', 'f', 'False', '0'):
+ elif data in self.FALSE_VALUES:
+ return False
+ self.fail('invalid', input=data)
+
+ def to_representation(self, value):
+ if value in self.TRUE_VALUES:
+ return True
+ elif value in self.FALSE_VALUES:
return False
return bool(value)
-class CharField(WritableField):
- type_name = 'CharField'
- type_label = 'string'
- form_field_class = forms.CharField
-
- def __init__(self, max_length=None, min_length=None, *args, **kwargs):
- self.max_length, self.min_length = max_length, min_length
- super(CharField, self).__init__(*args, **kwargs)
- if min_length is not None:
- self.validators.append(validators.MinLengthValidator(min_length))
- if max_length is not None:
- self.validators.append(validators.MaxLengthValidator(max_length))
-
- def from_native(self, value):
- if isinstance(value, six.string_types) or value is None:
- return value
- return smart_text(value)
-
-
-class URLField(CharField):
- type_name = 'URLField'
- type_label = 'url'
+class NullBooleanField(Field):
+ default_error_messages = {
+ 'invalid': _('"{input}" is not a valid boolean.')
+ }
+ initial = None
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
+ NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))
def __init__(self, **kwargs):
- kwargs['validators'] = [validators.URLValidator()]
- super(URLField, self).__init__(**kwargs)
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
+ kwargs['allow_null'] = True
+ super(NullBooleanField, self).__init__(**kwargs)
+
+ def to_internal_value(self, data):
+ if data in self.TRUE_VALUES:
+ return True
+ elif data in self.FALSE_VALUES:
+ return False
+ elif data in self.NULL_VALUES:
+ return None
+ self.fail('invalid', input=data)
+
+ def to_representation(self, value):
+ if value in self.NULL_VALUES:
+ return None
+ if value in self.TRUE_VALUES:
+ return True
+ elif value in self.FALSE_VALUES:
+ return False
+ return bool(value)
-class SlugField(CharField):
- type_name = 'SlugField'
- type_label = 'slug'
- form_field_class = forms.SlugField
+# String types...
+class CharField(Field):
default_error_messages = {
- 'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
- " underscores or hyphens."),
+ 'blank': _('This field may not be blank.'),
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ 'min_length': _('Ensure this field has at least {min_length} characters.')
}
- default_validators = [validators.validate_slug]
+ initial = ''
- def __init__(self, *args, **kwargs):
- super(SlugField, self).__init__(*args, **kwargs)
+ def __init__(self, **kwargs):
+ self.allow_blank = kwargs.pop('allow_blank', False)
+ self.trim_whitespace = kwargs.pop('trim_whitespace', True)
+ self.max_length = kwargs.pop('max_length', None)
+ self.min_length = kwargs.pop('min_length', None)
+ super(CharField, self).__init__(**kwargs)
+ if self.max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=self.max_length)
+ self.validators.append(MaxLengthValidator(self.max_length, message=message))
+ if self.min_length is not None:
+ message = self.error_messages['min_length'].format(min_length=self.min_length)
+ self.validators.append(MinLengthValidator(self.min_length, message=message))
+ def run_validation(self, data=empty):
+ # Test for the empty string here so that it does not get validated,
+ # and so that subclasses do not need to handle it explicitly
+ # inside the `to_internal_value()` method.
+ if data == '':
+ if not self.allow_blank:
+ self.fail('blank')
+ return ''
+ return super(CharField, self).run_validation(data)
-class ChoiceField(WritableField):
- type_name = 'ChoiceField'
- type_label = 'multiple choice'
- form_field_class = forms.ChoiceField
- widget = widgets.Select
- default_error_messages = {
- 'invalid_choice': _('Select a valid choice. %(value)s is not one of '
- 'the available choices.'),
- }
+ def to_internal_value(self, data):
+ value = six.text_type(data)
+ return value.strip() if self.trim_whitespace else value
- def __init__(self, choices=(), *args, **kwargs):
- super(ChoiceField, self).__init__(*args, **kwargs)
- self.choices = choices
- if not self.required:
- self.choices = BLANK_CHOICE_DASH + self.choices
-
- def _get_choices(self):
- return self._choices
-
- def _set_choices(self, value):
- # Setting choices also sets the choices on the widget.
- # choices can be any iterable, but we call list() on it because
- # it will be consumed more than once.
- self._choices = self.widget.choices = list(value)
-
- choices = property(_get_choices, _set_choices)
-
- def validate(self, value):
- """
- Validates that the input is in self.choices.
- """
- super(ChoiceField, self).validate(value)
- if value and not self.valid_value(value):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
-
- def valid_value(self, value):
- """
- Check to see if the provided value is a valid choice.
- """
- for k, v in self.choices:
- if isinstance(v, (list, tuple)):
- # This is an optgroup, so look inside the group for options
- for k2, v2 in v:
- if value == smart_text(k2):
- return True
- else:
- if value == smart_text(k) or value == k:
- return True
- return False
+ def to_representation(self, value):
+ return six.text_type(value)
class EmailField(CharField):
- type_name = 'EmailField'
- type_label = 'email'
- form_field_class = forms.EmailField
-
default_error_messages = {
- 'invalid': _('Enter a valid email address.'),
+ 'invalid': _('Enter a valid email address.')
}
- default_validators = [validators.validate_email]
- def from_native(self, value):
- ret = super(EmailField, self).from_native(value)
- if ret is None:
- return None
- return ret.strip()
+ def __init__(self, **kwargs):
+ super(EmailField, self).__init__(**kwargs)
+ validator = EmailValidator(message=self.error_messages['invalid'])
+ self.validators.append(validator)
class RegexField(CharField):
- type_name = 'RegexField'
- type_label = 'regex'
- form_field_class = forms.RegexField
-
- def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
- super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
- self.regex = regex
-
- def _get_regex(self):
- return self._regex
-
- def _set_regex(self, regex):
- if isinstance(regex, six.string_types):
- regex = re.compile(regex)
- self._regex = regex
- if hasattr(self, '_regex_validator') and self._regex_validator in self.validators:
- self.validators.remove(self._regex_validator)
- self._regex_validator = validators.RegexValidator(regex=regex)
- self.validators.append(self._regex_validator)
-
- regex = property(_get_regex, _set_regex)
-
-
-class DateField(WritableField):
- type_name = 'DateField'
- type_label = 'date'
- widget = widgets.DateInput
- form_field_class = forms.DateField
-
default_error_messages = {
- 'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('This value does not match the required pattern.')
}
- empty = None
- input_formats = api_settings.DATE_INPUT_FORMATS
- format = api_settings.DATE_FORMAT
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ def __init__(self, regex, **kwargs):
+ super(RegexField, self).__init__(**kwargs)
+ validator = RegexValidator(regex, message=self.error_messages['invalid'])
+ self.validators.append(validator)
+
+
+class SlugField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')
+ }
+
+ def __init__(self, **kwargs):
+ super(SlugField, self).__init__(**kwargs)
+ slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$')
+ validator = RegexValidator(slug_regex, message=self.error_messages['invalid'])
+ self.validators.append(validator)
+
+
+class URLField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid URL.')
+ }
+
+ def __init__(self, **kwargs):
+ super(URLField, self).__init__(**kwargs)
+ validator = URLValidator(message=self.error_messages['invalid'])
+ self.validators.append(validator)
+
+
+class UUIDField(Field):
+ default_error_messages = {
+ 'invalid': _('"{value}" is not a valid UUID.'),
+ }
+
+ def to_internal_value(self, data):
+ if not isinstance(data, uuid.UUID):
+ try:
+ return uuid.UUID(data)
+ except (ValueError, TypeError):
+ self.fail('invalid', value=data)
+ return data
+
+ def to_representation(self, value):
+ return str(value)
+
+
+# Number types...
+
+class IntegerField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid integer is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
+ }
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
+
+ def __init__(self, **kwargs):
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
+ super(IntegerField, self).__init__(**kwargs)
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+
+ def to_internal_value(self, data):
+ if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
+
+ try:
+ data = int(data)
+ except (ValueError, TypeError):
+ self.fail('invalid')
+ return data
+
+ def to_representation(self, value):
+ return int(value)
+
+
+class FloatField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
+ }
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
+
+ def __init__(self, **kwargs):
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
+ super(FloatField, self).__init__(**kwargs)
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+
+ def to_internal_value(self, data):
+ if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
+
+ try:
+ return float(data)
+ except (TypeError, ValueError):
+ self.fail('invalid')
+
+ def to_representation(self, value):
+ return float(value)
+
+
+class DecimalField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
+ 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
+ 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'),
+ 'max_string_length': _('String value too large.')
+ }
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
+
+ coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING
+
+ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs):
+ self.max_digits = max_digits
+ self.decimal_places = decimal_places
+ self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
+
+ self.max_value = max_value
+ self.min_value = min_value
+
+ super(DecimalField, self).__init__(**kwargs)
+
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+
+ def to_internal_value(self, data):
+ """
+ Validates that the input is a decimal number. Returns a Decimal
+ instance. Returns None for empty values. Ensures that there are no more
+ than max_digits in the number, and no more than decimal_places digits
+ after the decimal point.
+ """
+ data = smart_text(data).strip()
+ if len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
+
+ try:
+ value = decimal.Decimal(data)
+ except decimal.DecimalException:
+ self.fail('invalid')
+
+ # Check for NaN. It is the only value that isn't equal to itself,
+ # so we can use this to identify NaN values.
+ if value != value:
+ self.fail('invalid')
+
+ # Check for infinity and negative infinity.
+ if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
+ self.fail('invalid')
+
+ sign, digittuple, exponent = value.as_tuple()
+ decimals = abs(exponent)
+ # digittuple doesn't include any leading zeros.
+ digits = len(digittuple)
+ if decimals > digits:
+ # We have leading zeros up to or past the decimal point. Count
+ # everything past the decimal point as a digit. We do not count
+ # 0 before the decimal point as a digit since that would mean
+ # we would not allow max_digits = decimal_places.
+ digits = decimals
+ whole_digits = digits - decimals
+
+ if self.max_digits is not None and digits > self.max_digits:
+ self.fail('max_digits', max_digits=self.max_digits)
+ if self.decimal_places is not None and decimals > self.decimal_places:
+ self.fail('max_decimal_places', max_decimal_places=self.decimal_places)
+ if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
+ self.fail('max_whole_digits', max_whole_digits=self.max_digits - self.decimal_places)
+
+ return value
+
+ def to_representation(self, value):
+ if not isinstance(value, decimal.Decimal):
+ value = decimal.Decimal(six.text_type(value).strip())
+
+ context = decimal.getcontext().copy()
+ context.prec = self.max_digits
+ quantized = value.quantize(
+ decimal.Decimal('.1') ** self.decimal_places,
+ context=context
+ )
+ if not self.coerce_to_string:
+ return quantized
+ return '{0:f}'.format(quantized)
+
+
+# Date & time fields...
+
+class DateTimeField(Field):
+ default_error_messages = {
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
+ 'date': _('Expected a datetime but got a date.'),
+ }
+ format = api_settings.DATETIME_FORMAT
+ input_formats = api_settings.DATETIME_INPUT_FORMATS
+ default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
+
+ def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateField, self).__init__(*args, **kwargs)
+ self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
+ super(DateTimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ def enforce_timezone(self, value):
+ """
+ When `self.default_timezone` is `None`, always return naive datetimes.
+ When `self.default_timezone` is not `None`, always return aware datetimes.
+ """
+ if (self.default_timezone is not None) and not timezone.is_aware(value):
+ return timezone.make_aware(value, self.default_timezone)
+ elif (self.default_timezone is None) and timezone.is_aware(value):
+ return timezone.make_naive(value, timezone.UTC())
+ return value
+
+ def to_internal_value(self, value):
+ if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
+ self.fail('date')
if isinstance(value, datetime.datetime):
- if timezone and settings.USE_TZ and timezone.is_aware(value):
- # Convert aware datetimes to the default time zone
- # before casting them to dates (#17742).
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_naive(value, default_timezone)
- return value.date()
+ return self.enforce_timezone(value)
+
+ for format in self.input_formats:
+ if format.lower() == ISO_8601:
+ try:
+ parsed = parse_datetime(value)
+ except (ValueError, TypeError):
+ pass
+ else:
+ if parsed is not None:
+ return self.enforce_timezone(parsed)
+ else:
+ try:
+ parsed = datetime.datetime.strptime(value, format)
+ except (ValueError, TypeError):
+ pass
+ else:
+ return self.enforce_timezone(parsed)
+
+ humanized_format = humanize_datetime.datetime_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
+
+ def to_representation(self, value):
+ if self.format is None:
+ return value
+
+ if self.format.lower() == ISO_8601:
+ value = value.isoformat()
+ if value.endswith('+00:00'):
+ value = value[:-6] + 'Z'
+ return value
+ return value.strftime(self.format)
+
+
+class DateField(Field):
+ default_error_messages = {
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
+ 'datetime': _('Expected a date but got a datetime.'),
+ }
+ format = api_settings.DATE_FORMAT
+ input_formats = api_settings.DATE_INPUT_FORMATS
+
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
+ self.input_formats = input_formats if input_formats is not None else self.input_formats
+ super(DateField, self).__init__(*args, **kwargs)
+
+ def to_internal_value(self, value):
+ if isinstance(value, datetime.datetime):
+ self.fail('datetime')
+
if isinstance(value, datetime.date):
return value
@@ -596,113 +920,40 @@ class DateField(WritableField):
else:
return parsed.date()
- msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats)
- raise ValidationError(msg)
+ humanized_format = humanize_datetime.date_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
- def to_native(self, value):
- if value is None or self.format is None:
+ def to_representation(self, value):
+ if self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.date()
+ # Applying a `DateField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `date`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
if self.format.lower() == ISO_8601:
return value.isoformat()
return value.strftime(self.format)
-class DateTimeField(WritableField):
- type_name = 'DateTimeField'
- type_label = 'datetime'
- widget = widgets.DateTimeInput
- form_field_class = forms.DateTimeField
-
+class TimeField(Field):
default_error_messages = {
- 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
}
- empty = None
- input_formats = api_settings.DATETIME_INPUT_FORMATS
- format = api_settings.DATETIME_FORMAT
-
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
- self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateTimeField, self).__init__(*args, **kwargs)
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- if isinstance(value, datetime.datetime):
- return value
- if isinstance(value, datetime.date):
- value = datetime.datetime(value.year, value.month, value.day)
- if settings.USE_TZ:
- # For backwards compatibility, interpret naive datetimes in
- # local time. This won't work during DST change, but we can't
- # do much about it, so we let the exceptions percolate up the
- # call stack.
- warnings.warn("DateTimeField received a naive datetime (%s)"
- " while time zone support is active." % value,
- RuntimeWarning)
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_aware(value, default_timezone)
- return value
-
- for format in self.input_formats:
- if format.lower() == ISO_8601:
- try:
- parsed = parse_datetime(value)
- except (ValueError, TypeError):
- pass
- else:
- if parsed is not None:
- return parsed
- else:
- try:
- parsed = datetime.datetime.strptime(value, format)
- except (ValueError, TypeError):
- pass
- else:
- return parsed
-
- msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats)
- raise ValidationError(msg)
-
- def to_native(self, value):
- if value is None or self.format is None:
- return value
-
- if self.format.lower() == ISO_8601:
- ret = value.isoformat()
- if ret.endswith('+00:00'):
- ret = ret[:-6] + 'Z'
- return ret
- return value.strftime(self.format)
-
-
-class TimeField(WritableField):
- type_name = 'TimeField'
- type_label = 'time'
- widget = widgets.TimeInput
- form_field_class = forms.TimeField
-
- default_error_messages = {
- 'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
- }
- empty = None
- input_formats = api_settings.TIME_INPUT_FORMATS
format = api_settings.TIME_FORMAT
+ input_formats = api_settings.TIME_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
super(TimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
+ def to_internal_value(self, value):
if isinstance(value, datetime.time):
return value
@@ -723,246 +974,389 @@ class TimeField(WritableField):
else:
return parsed.time()
- msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats)
- raise ValidationError(msg)
+ humanized_format = humanize_datetime.time_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
- def to_native(self, value):
- if value is None or self.format is None:
+ def to_representation(self, value):
+ if self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.time()
+ # Applying a `TimeField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `time`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
if self.format.lower() == ISO_8601:
return value.isoformat()
return value.strftime(self.format)
-class IntegerField(WritableField):
- type_name = 'IntegerField'
- type_label = 'integer'
- form_field_class = forms.IntegerField
+# Choice types...
+class ChoiceField(Field):
default_error_messages = {
- 'invalid': _('Enter a whole number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
+ 'invalid_choice': _('"{input}" is not a valid choice.')
}
- def __init__(self, max_value=None, min_value=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- super(IntegerField, self).__init__(*args, **kwargs)
+ def __init__(self, choices, **kwargs):
+ # Allow either single or paired choices style:
+ # choices = [1, 2, 3]
+ # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
+ pairs = [
+ isinstance(item, (list, tuple)) and len(item) == 2
+ for item in choices
+ ]
+ if all(pairs):
+ self.choices = OrderedDict([(key, display_value) for key, display_value in choices])
+ else:
+ self.choices = OrderedDict([(item, item) for item in choices])
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ # Map the string representation of choices to the underlying value.
+ # Allows us to deal with eg. integer choices while supporting either
+ # integer or string input, but still get the correct datatype out.
+ self.choice_strings_to_values = dict([
+ (six.text_type(key), key) for key in self.choices.keys()
+ ])
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ self.allow_blank = kwargs.pop('allow_blank', False)
+
+ super(ChoiceField, self).__init__(**kwargs)
+
+ def to_internal_value(self, data):
+ if data == '' and self.allow_blank:
+ return ''
try:
- value = int(str(value))
- except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
- return value
+ return self.choice_strings_to_values[six.text_type(data)]
+ except KeyError:
+ self.fail('invalid_choice', input=data)
+
+ def to_representation(self, value):
+ if value in ('', None):
+ return value
+ return self.choice_strings_to_values[six.text_type(value)]
-class FloatField(WritableField):
- type_name = 'FloatField'
- type_label = 'float'
- form_field_class = forms.FloatField
-
+class MultipleChoiceField(ChoiceField):
default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
+ 'invalid_choice': _('"{input}" is not a valid choice.'),
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
+ default_empty_html = []
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return dictionary.getlist(self.field_name)
+ return dictionary.get(self.field_name, empty)
- try:
- return float(value)
- except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise ValidationError(msg)
+ def to_internal_value(self, data):
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
+
+ return set([
+ super(MultipleChoiceField, self).to_internal_value(item)
+ for item in data
+ ])
+
+ def to_representation(self, value):
+ return set([
+ self.choice_strings_to_values[six.text_type(item)] for item in value
+ ])
-class DecimalField(WritableField):
- type_name = 'DecimalField'
- type_label = 'decimal'
- form_field_class = forms.DecimalField
+# File types...
+class FileField(Field):
default_error_messages = {
- 'invalid': _('Enter a number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
- 'max_digits': _('Ensure that there are no more than %s digits in total.'),
- 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
- 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
- }
-
- def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- self.max_digits, self.decimal_places = max_digits, decimal_places
- super(DecimalField, self).__init__(*args, **kwargs)
-
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
-
- def from_native(self, value):
- """
- Validates that the input is a decimal number. Returns a Decimal
- instance. Returns None for empty values. Ensures that there are no more
- than max_digits in the number, and no more than decimal_places digits
- after the decimal point.
- """
- if value in validators.EMPTY_VALUES:
- return None
- value = smart_text(value).strip()
- try:
- value = Decimal(value)
- except DecimalException:
- raise ValidationError(self.error_messages['invalid'])
- return value
-
- def validate(self, value):
- super(DecimalField, self).validate(value)
- if value in validators.EMPTY_VALUES:
- return
- # Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
- # since it is never equal to itself. However, NaN is the only value that
- # isn't equal to itself, so we can use this to identify NaN
- if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
- raise ValidationError(self.error_messages['invalid'])
- sign, digittuple, exponent = value.as_tuple()
- decimals = abs(exponent)
- # digittuple doesn't include any leading zeros.
- digits = len(digittuple)
- if decimals > digits:
- # We have leading zeros up to or past the decimal point. Count
- # everything past the decimal point as a digit. We do not count
- # 0 before the decimal point as a digit since that would mean
- # we would not allow max_digits = decimal_places.
- digits = decimals
- whole_digits = digits - decimals
-
- if self.max_digits is not None and digits > self.max_digits:
- raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
- if self.decimal_places is not None and decimals > self.decimal_places:
- raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
- if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
- raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
- return value
-
-
-class FileField(WritableField):
- use_files = True
- type_name = 'FileField'
- type_label = 'file upload'
- form_field_class = forms.FileField
- widget = widgets.FileInput
-
- default_error_messages = {
- 'invalid': _("No file was submitted. Check the encoding type on the form."),
- 'missing': _("No file was submitted."),
- 'empty': _("The submitted file is empty."),
- 'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
- 'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
+ 'required': _('No file was submitted.'),
+ 'invalid': _('The submitted data was not a file. Check the encoding type on the form.'),
+ 'no_name': _('No filename could be determined.'),
+ 'empty': _('The submitted file is empty.'),
+ 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
}
+ use_url = api_settings.UPLOADED_FILES_USE_URL
def __init__(self, *args, **kwargs):
self.max_length = kwargs.pop('max_length', None)
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
+ self.use_url = kwargs.pop('use_url', self.use_url)
super(FileField, self).__init__(*args, **kwargs)
- def from_native(self, data):
- if data in validators.EMPTY_VALUES:
- return None
-
- # UploadedFile objects should have name and size attributes.
+ def to_internal_value(self, data):
try:
+ # `UploadedFile` objects should have name and size attributes.
file_name = data.name
file_size = data.size
except AttributeError:
- raise ValidationError(self.error_messages['invalid'])
+ self.fail('invalid')
- if self.max_length is not None and len(file_name) > self.max_length:
- error_values = {'max': self.max_length, 'length': len(file_name)}
- raise ValidationError(self.error_messages['max_length'] % error_values)
if not file_name:
- raise ValidationError(self.error_messages['invalid'])
+ self.fail('no_name')
if not self.allow_empty_file and not file_size:
- raise ValidationError(self.error_messages['empty'])
+ self.fail('empty')
+ if self.max_length and len(file_name) > self.max_length:
+ self.fail('max_length', max_length=self.max_length, length=len(file_name))
return data
- def to_native(self, value):
+ def to_representation(self, value):
+ if self.use_url:
+ if not value:
+ return None
+ url = value.url
+ request = self.context.get('request', None)
+ if request is not None:
+ return request.build_absolute_uri(url)
+ return url
return value.name
class ImageField(FileField):
- use_files = True
- type_name = 'ImageField'
- type_label = 'image upload'
- form_field_class = forms.ImageField
-
default_error_messages = {
- 'invalid_image': _("Upload a valid image. The file you uploaded was "
- "either not an image or a corrupted image."),
+ 'invalid_image': _(
+ 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
+ ),
}
- def from_native(self, data):
+ def __init__(self, *args, **kwargs):
+ self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
+ super(ImageField, self).__init__(*args, **kwargs)
+
+ def to_internal_value(self, data):
+ # Image validation is a bit grungy, so we'll just outright
+ # defer to Django's implementation so we don't need to
+ # consider it, or treat PIL as a test dependency.
+ file_object = super(ImageField, self).to_internal_value(data)
+ django_field = self._DjangoImageField()
+ django_field.error_messages = self.error_messages
+ django_field.to_python(file_object)
+ return file_object
+
+
+# Composite field types...
+
+class _UnvalidatedField(Field):
+ def __init__(self, *args, **kwargs):
+ super(_UnvalidatedField, self).__init__(*args, **kwargs)
+ self.allow_blank = True
+ self.allow_null = True
+
+ def to_internal_value(self, data):
+ return data
+
+ def to_representation(self, value):
+ return value
+
+
+class ListField(Field):
+ child = _UnvalidatedField()
+ initial = []
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListField, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
"""
- Checks that the file-upload field data contains a valid image (GIF, JPG,
- PNG, possibly others -- whatever the Python Imaging Library supports).
+ List of dicts of native values <- List of dicts of primitive datatypes.
"""
- f = super(ImageField, self).from_native(data)
- if f is None:
- return None
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
+ return [self.child.run_validation(item) for item in data]
- from compat import Image
- assert Image is not None, 'PIL must be installed for ImageField support'
+ def to_representation(self, data):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ return [self.child.to_representation(item) for item in data]
- # We need to get a file object for PIL. We might have a path or we might
- # have to read the data into memory.
- if hasattr(data, 'temporary_file_path'):
- file = data.temporary_file_path()
- else:
- if hasattr(data, 'read'):
- file = BytesIO(data.read())
- else:
- file = BytesIO(data['content'])
- try:
- # load() could spot a truncated JPEG, but it loads the entire
- # image in memory, which is a DoS vector. See #3848 and #18520.
- # verify() must be called immediately after the constructor.
- Image.open(file).verify()
- except ImportError:
- # Under PyPy, it is possible to import PIL. However, the underlying
- # _imaging C module isn't available, so an ImportError will be
- # raised. Catch and re-raise.
- raise
- except Exception: # Python Imaging Library doesn't recognize it as an image
- raise ValidationError(self.error_messages['invalid_image'])
- if hasattr(f, 'seek') and callable(f.seek):
- f.seek(0)
- return f
+class DictField(Field):
+ child = _UnvalidatedField()
+ initial = {}
+ default_error_messages = {
+ 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".')
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(DictField, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # dictionaries in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
+ """
+ Dicts of native values <- Dicts of primitive datatypes.
+ """
+ if html.is_html_input(data):
+ data = html.parse_html_dict(data)
+ if not isinstance(data, dict):
+ self.fail('not_a_dict', input_type=type(data).__name__)
+ return dict([
+ (six.text_type(key), self.child.run_validation(value))
+ for key, value in data.items()
+ ])
+
+ def to_representation(self, value):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ return dict([
+ (six.text_type(key), self.child.to_representation(val))
+ for key, val in value.items()
+ ])
+
+
+# Miscellaneous field types...
+
+class ReadOnlyField(Field):
+ """
+ A read-only field that simply returns the field value.
+
+ If the field is a method with no parameters, the method will be called
+ and it's return value used as the representation.
+
+ For example, the following would call `get_expiry_date()` on the object:
+
+ class ExampleSerializer(self):
+ expiry_date = ReadOnlyField(source='get_expiry_date')
+ """
+
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ super(ReadOnlyField, self).__init__(**kwargs)
+
+ def to_representation(self, value):
+ return value
+
+
+class HiddenField(Field):
+ """
+ A hidden field does not take input from the user, or present any output,
+ but it does populate a field in `validated_data`, based on its default
+ value. This is particularly useful when we have a `unique_for_date`
+ constraint on a pair of fields, as we need some way to include the date in
+ the validated data.
+ """
+ def __init__(self, **kwargs):
+ assert 'default' in kwargs, 'default is a required argument.'
+ kwargs['write_only'] = True
+ super(HiddenField, self).__init__(**kwargs)
+
+ def get_value(self, dictionary):
+ # We always use the default value for `HiddenField`.
+ # User input is never provided or accepted.
+ return empty
+
+ def to_internal_value(self, data):
+ return data
class SerializerMethodField(Field):
"""
- A field that gets its value by calling a method on the serializer it's attached to.
+ A read-only field that get its representation from calling a method on the
+ parent serializer class. The method called will be of the form
+ "get_{field_name}", and should take a single argument, which is the
+ object being serialized.
+
+ For example:
+
+ class ExampleSerializer(self):
+ extra_info = SerializerMethodField()
+
+ def get_extra_info(self, obj):
+ return ... # Calculate some data to return.
"""
-
- def __init__(self, method_name):
+ def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
- super(SerializerMethodField, self).__init__()
+ kwargs['source'] = '*'
+ kwargs['read_only'] = True
+ super(SerializerMethodField, self).__init__(**kwargs)
- def field_to_native(self, obj, field_name):
- value = getattr(self.parent, self.method_name)(obj)
- return self.to_native(value)
+ def bind(self, field_name, parent):
+ # In order to enforce a consistent style, we error if a redundant
+ # 'method_name' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ default_method_name = 'get_{field_name}'.format(field_name=field_name)
+ assert self.method_name != default_method_name, (
+ "It is redundant to specify `%s` on SerializerMethodField '%s' in "
+ "serializer '%s', because it is the same as the default method name. "
+ "Remove the `method_name` argument." %
+ (self.method_name, field_name, parent.__class__.__name__)
+ )
+
+ # The method name should default to `get_{field_name}`.
+ if self.method_name is None:
+ self.method_name = default_method_name
+
+ super(SerializerMethodField, self).bind(field_name, parent)
+
+ def to_representation(self, value):
+ method = getattr(self.parent, self.method_name)
+ return method(value)
+
+
+class ModelField(Field):
+ """
+ A generic field that can be used against an arbitrary model field.
+
+ This is used by `ModelSerializer` when dealing with custom model fields,
+ that do not have a serializer field to be mapped to.
+ """
+ default_error_messages = {
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ }
+
+ def __init__(self, model_field, **kwargs):
+ self.model_field = model_field
+ # The `max_length` option is supported by Django's base `Field` class,
+ # so we'd better support it here.
+ max_length = kwargs.pop('max_length', None)
+ super(ModelField, self).__init__(**kwargs)
+ if max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=max_length)
+ self.validators.append(MaxLengthValidator(max_length, message=message))
+
+ def to_internal_value(self, data):
+ rel = getattr(self.model_field, 'rel', None)
+ if rel is not None:
+ return rel.to._meta.get_field(rel.field_name).to_python(data)
+ return self.model_field.to_python(data)
+
+ def get_attribute(self, obj):
+ # We pass the object instance onto `to_representation`,
+ # not just the field attribute.
+ return obj
+
+ def to_representation(self, obj):
+ value = self.model_field._get_val_from_obj(obj)
+ if is_protected_type(value):
+ return value
+ return self.model_field.value_to_string(obj)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c058bc715..9a84efa23 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -3,8 +3,12 @@ Provides generic filtering backends that can be used to filter the results
returned by list views.
"""
from __future__ import unicode_literals
+
+from django.core.exceptions import ImproperlyConfigured
from django.db import models
-from rest_framework.compat import django_filters, six
+from django.utils import six
+from rest_framework.compat import django_filters, guardian, get_model_name
+from rest_framework.settings import api_settings
from functools import reduce
import operator
@@ -42,7 +46,7 @@ class DjangoFilterBackend(BaseFilterBackend):
if filter_class:
filter_model = filter_class.Meta.model
- assert issubclass(filter_model, queryset.model), \
+ assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
@@ -61,20 +65,21 @@ class DjangoFilterBackend(BaseFilterBackend):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
- return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
+ return filter_class(request.query_params, queryset=queryset).qs
return queryset
class SearchFilter(BaseFilterBackend):
- search_param = 'search' # The URL query parameter used for the search.
+ # The URL query parameter used for the search.
+ search_param = api_settings.SEARCH_PARAM
def get_search_terms(self, request):
"""
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
- params = request.QUERY_PARAMS.get(self.search_param, '')
+ params = request.query_params.get(self.search_param, '')
return params.replace(',', ' ').split()
def construct_search(self, field_name):
@@ -93,28 +98,39 @@ class SearchFilter(BaseFilterBackend):
if not search_fields:
return queryset
- orm_lookups = [self.construct_search(str(search_field))
+ orm_lookups = [self.construct_search(six.text_type(search_field))
for search_field in search_fields]
for search_term in self.get_search_terms(request):
or_queries = [models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups]
- queryset = queryset.filter(reduce(operator.or_, or_queries))
+ queryset = queryset.filter(reduce(operator.or_, or_queries)).distinct()
return queryset
class OrderingFilter(BaseFilterBackend):
- ordering_param = 'ordering' # The URL query parameter used for the ordering.
+ # The URL query parameter used for the ordering.
+ ordering_param = api_settings.ORDERING_PARAM
+ ordering_fields = None
- def get_ordering(self, request):
+ def get_ordering(self, request, queryset, view):
"""
- Search terms are set by a ?search=... query parameter,
- and may be comma and/or whitespace delimited.
+ Ordering is set by a comma delimited ?ordering=... query parameter.
+
+ The `ordering` query parameter can be overridden by setting
+ the `ordering_param` value on the OrderingFilter or by
+ specifying an `ORDERING_PARAM` value in the API settings.
"""
- params = request.QUERY_PARAMS.get(self.ordering_param)
+ params = request.query_params.get(self.ordering_param)
if params:
- return [param.strip() for param in params.split(',')]
+ fields = [param.strip() for param in params.split(',')]
+ ordering = self.remove_invalid_fields(queryset, fields, view)
+ if ordering:
+ return ordering
+
+ # No ordering was included, or all the ordering fields were invalid
+ return self.get_default_ordering(view)
def get_default_ordering(self, view):
ordering = getattr(view, 'ordering', None)
@@ -122,22 +138,53 @@ class OrderingFilter(BaseFilterBackend):
return (ordering,)
return ordering
- def remove_invalid_fields(self, queryset, ordering):
- field_names = [field.name for field in queryset.model._meta.fields]
- return [term for term in ordering if term.lstrip('-') in field_names]
+ def remove_invalid_fields(self, queryset, fields, view):
+ valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
+
+ if valid_fields is None:
+ # Default to allowing filtering on serializer fields
+ serializer_class = getattr(view, 'serializer_class')
+ if serializer_class is None:
+ msg = ("Cannot use %s on a view which does not have either a "
+ "'serializer_class' or 'ordering_fields' attribute.")
+ raise ImproperlyConfigured(msg % self.__class__.__name__)
+ valid_fields = [
+ field.source or field_name
+ for field_name, field in serializer_class().fields.items()
+ if not getattr(field, 'write_only', False)
+ ]
+ elif valid_fields == '__all__':
+ # View explicitly allows filtering on any model field
+ valid_fields = [field.name for field in queryset.model._meta.fields]
+ valid_fields += queryset.query.aggregates.keys()
+
+ return [term for term in fields if term.lstrip('-') in valid_fields]
def filter_queryset(self, request, queryset, view):
- ordering = self.get_ordering(request)
-
- if ordering:
- # Skip any incorrect parameters
- ordering = self.remove_invalid_fields(queryset, ordering)
-
- if not ordering:
- # Use 'ordering' attribtue by default
- ordering = self.get_default_ordering(view)
+ ordering = self.get_ordering(request, queryset, view)
if ordering:
return queryset.order_by(*ordering)
return queryset
+
+
+class DjangoObjectPermissionsFilter(BaseFilterBackend):
+ """
+ A filter backend that limits results to those where the requesting user
+ has read object level permissions.
+ """
+ def __init__(self):
+ assert guardian, 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
+
+ perm_format = '%(app_label)s.view_%(model_name)s'
+
+ def filter_queryset(self, request, queryset, view):
+ user = request.user
+ model_cls = queryset.model
+ kwargs = {
+ 'app_label': model_cls._meta.app_label,
+ 'model_name': get_model_name(model_cls)
+ }
+ permission = self.perm_format % kwargs
+ return guardian.shortcuts.get_objects_for_user(user, permission, queryset)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 99e9782e2..61dcb84a4 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -2,25 +2,20 @@
Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
-
-from django.core.exceptions import ImproperlyConfigured, PermissionDenied
-from django.core.paginator import Paginator, InvalidPage
+from django.db.models.query import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404
-from django.utils.translation import ugettext as _
-from rest_framework import views, mixins, exceptions
-from rest_framework.request import clone_request
+from rest_framework import views, mixins
from rest_framework.settings import api_settings
-import warnings
-def get_object_or_404(queryset, **filter_kwargs):
+def get_object_or_404(queryset, *filter_args, **filter_kwargs):
"""
- Same as Django's standard shortcut, but make sure to raise 404
+ Same as Django's standard shortcut, but make sure to also raise 404
if the filter_kwargs don't match the required types.
"""
try:
- return _get_object_or_404(queryset, **filter_kwargs)
+ return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
except (TypeError, ValueError):
raise Http404
@@ -29,180 +24,89 @@ class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
-
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
+ # If you are overriding a view method, it is important that you call
+ # `get_queryset()` instead of accessing the `queryset` property directly,
+ # as `queryset` will get evaluated only once, and those results are cached
+ # for all subsequent requests.
queryset = None
serializer_class = None
- # This shortcut may be used instead of setting either or both
- # of the `queryset`/`serializer_class` attributes, although using
- # the explicit style is generally preferred.
- model = None
-
- # If you want to use object lookups other than pk, set this attribute.
+ # If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
-
- # Pagination settings
- paginate_by = api_settings.PAGINATE_BY
- paginate_by_param = api_settings.PAGINATE_BY_PARAM
- pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
- page_kwarg = 'page'
+ lookup_url_kwarg = None
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
- # The following attributes may be subject to change,
- # and should be considered private API.
- model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
- paginator_class = Paginator
+ # The style to use for queryset pagination.
+ pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
- ######################################
- # These are pending deprecation...
-
- pk_url_kwarg = 'pk'
- slug_url_kwarg = 'slug'
- slug_field = 'slug'
- allow_empty = True
- filter_backend = api_settings.FILTER_BACKEND
-
- def get_serializer_context(self):
+ def get_queryset(self):
"""
- Extra context provided to the serializer class.
- """
- return {
- 'request': self.request,
- 'format': self.format_kwarg,
- 'view': self
- }
+ Get the list of items for this view.
+ This must be an iterable, and may be a queryset.
+ Defaults to using `self.queryset`.
- def get_serializer(self, instance=None, data=None,
- files=None, many=False, partial=False):
+ This method should always be used rather than accessing `self.queryset`
+ directly, as `self.queryset` gets evaluated only once, and those results
+ are cached for all subsequent requests.
+
+ You may want to override this if you need to provide different
+ querysets depending on the incoming request.
+
+ (Eg. return a list of items that is specific to the user)
+ """
+ assert self.queryset is not None, (
+ "'%s' should either include a `queryset` attribute, "
+ "or override the `get_queryset()` method."
+ % self.__class__.__name__
+ )
+
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated on each request.
+ queryset = queryset.all()
+ return queryset
+
+ def get_object(self):
+ """
+ Returns the object the view is displaying.
+
+ You may want to override this if you need to provide non-standard
+ queryset lookups. Eg if objects are referenced using multiple
+ keyword arguments in the url conf.
+ """
+ queryset = self.filter_queryset(self.get_queryset())
+
+ # Perform the lookup filtering.
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+
+ assert lookup_url_kwarg in self.kwargs, (
+ 'Expected view %s to be called with a URL keyword argument '
+ 'named "%s". Fix your URL conf, or set the `.lookup_field` '
+ 'attribute on the view correctly.' %
+ (self.__class__.__name__, lookup_url_kwarg)
+ )
+
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
+ obj = get_object_or_404(queryset, **filter_kwargs)
+
+ # May raise a permission denied
+ self.check_object_permissions(self.request, obj)
+
+ return obj
+
+ def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
- context = self.get_serializer_context()
- return serializer_class(instance, data=data, files=files,
- many=many, partial=partial, context=context)
-
- def get_pagination_serializer(self, page):
- """
- Return a serializer instance to use with paginated data.
- """
- class SerializerClass(self.pagination_serializer_class):
- class Meta:
- object_serializer_class = self.get_serializer_class()
-
- pagination_serializer_class = SerializerClass
- context = self.get_serializer_context()
- return pagination_serializer_class(instance=page, context=context)
-
- def paginate_queryset(self, queryset, page_size=None):
- """
- Paginate a queryset if required, either returning a page object,
- or `None` if pagination is not configured for this view.
- """
- deprecated_style = False
- if page_size is not None:
- warnings.warn('The `page_size` parameter to `paginate_queryset()` '
- 'is due to be deprecated. '
- 'Note that the return style of this method is also '
- 'changed, and will simply return a page object '
- 'when called without a `page_size` argument.',
- PendingDeprecationWarning, stacklevel=2)
- deprecated_style = True
- else:
- # Determine the required page size.
- # If pagination is not configured, simply return None.
- page_size = self.get_paginate_by()
- if not page_size:
- return None
-
- if not self.allow_empty:
- warnings.warn(
- 'The `allow_empty` parameter is due to be deprecated. '
- 'To use `allow_empty=False` style behavior, You should override '
- '`get_queryset()` and explicitly raise a 404 on empty querysets.',
- PendingDeprecationWarning, stacklevel=2
- )
-
- paginator = self.paginator_class(queryset, page_size,
- allow_empty_first_page=self.allow_empty)
- page_kwarg = self.kwargs.get(self.page_kwarg)
- page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
- page = page_kwarg or page_query_param or 1
- try:
- page_number = int(page)
- except ValueError:
- if page == 'last':
- page_number = paginator.num_pages
- else:
- raise Http404(_("Page is not 'last', nor can it be converted to an int."))
- try:
- page = paginator.page(page_number)
- except InvalidPage as e:
- raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
- 'page_number': page_number,
- 'message': str(e)
- })
-
- if deprecated_style:
- return (paginator, page, page.object_list, page.has_other_pages())
- return page
-
- def filter_queryset(self, queryset):
- """
- Given a queryset, filter it with whichever filter backend is in use.
-
- You are unlikely to want to override this method, although you may need
- to call it either from a list view, or from a custom `get_object`
- method if you want to apply the configured filtering backend to the
- default queryset.
- """
- filter_backends = self.filter_backends or []
- if not filter_backends and self.filter_backend:
- warnings.warn(
- 'The `filter_backend` attribute and `FILTER_BACKEND` setting '
- 'are due to be deprecated in favor of a `filter_backends` '
- 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
- 'a *list* of filter backend classes.',
- PendingDeprecationWarning, stacklevel=2
- )
- filter_backends = [self.filter_backend]
-
- for backend in filter_backends:
- queryset = backend().filter_queryset(self.request, queryset, self)
- return queryset
-
- ########################
- ### The following methods provide default implementations
- ### that you may want to override for more complex cases.
-
- def get_paginate_by(self, queryset=None):
- """
- Return the size of pages to use with pagination.
-
- If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
- from a named query parameter in the url, eg. ?page_size=100
-
- Otherwise defaults to using `self.paginate_by`.
- """
- if queryset is not None:
- warnings.warn('The `queryset` parameter to `get_paginate_by()` '
- 'is due to be deprecated.',
- PendingDeprecationWarning, stacklevel=2)
-
- if self.paginate_by_param:
- query_params = self.request.QUERY_PARAMS
- try:
- return int(query_params[self.paginate_by_param])
- except (KeyError, ValueError):
- pass
-
- return self.paginate_by
+ kwargs['context'] = self.get_serializer_context()
+ return serializer_class(*args, **kwargs)
def get_serializer_class(self):
"""
@@ -214,153 +118,67 @@ class GenericAPIView(views.APIView):
(Eg. admins get full serialization, others get basic serialization)
"""
- serializer_class = self.serializer_class
- if serializer_class is not None:
- return serializer_class
-
- assert self.model is not None, \
- "'%s' should either include a 'serializer_class' attribute, " \
- "or use the 'model' attribute as a shortcut for " \
- "automatically generating a serializer class." \
+ assert self.serializer_class is not None, (
+ "'%s' should either include a `serializer_class` attribute, "
+ "or override the `get_serializer_class()` method."
% self.__class__.__name__
+ )
- class DefaultSerializer(self.model_serializer_class):
- class Meta:
- model = self.model
- return DefaultSerializer
+ return self.serializer_class
- def get_queryset(self):
+ def get_serializer_context(self):
"""
- Get the list of items for this view.
- This must be an iterable, and may be a queryset.
- Defaults to using `self.queryset`.
-
- You may want to override this if you need to provide different
- querysets depending on the incoming request.
-
- (Eg. return a list of items that is specific to the user)
+ Extra context provided to the serializer class.
"""
- if self.queryset is not None:
- return self.queryset._clone()
+ return {
+ 'request': self.request,
+ 'format': self.format_kwarg,
+ 'view': self
+ }
- if self.model is not None:
- return self.model._default_manager.all()
-
- raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'"
- % self.__class__.__name__)
-
- def get_object(self, queryset=None):
+ def filter_queryset(self, queryset):
"""
- Returns the object the view is displaying.
+ Given a queryset, filter it with whichever filter backend is in use.
- You may want to override this if you need to provide non-standard
- queryset lookups. Eg if objects are referenced using multiple
- keyword arguments in the url conf.
+ You are unlikely to want to override this method, although you may need
+ to call it either from a list view, or from a custom `get_object`
+ method if you want to apply the configured filtering backend to the
+ default queryset.
"""
- # Determine the base queryset to use.
- if queryset is None:
- queryset = self.filter_queryset(self.get_queryset())
- else:
- pass # Deprecation warning
+ for backend in list(self.filter_backends):
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ return queryset
- # Perform the lookup filtering.
- pk = self.kwargs.get(self.pk_url_kwarg, None)
- slug = self.kwargs.get(self.slug_url_kwarg, None)
- lookup = self.kwargs.get(self.lookup_field, None)
-
- if lookup is not None:
- filter_kwargs = {self.lookup_field: lookup}
- elif pk is not None and self.lookup_field == 'pk':
- warnings.warn(
- 'The `pk_url_kwarg` attribute is due to be deprecated. '
- 'Use the `lookup_field` attribute instead',
- PendingDeprecationWarning
- )
- filter_kwargs = {'pk': pk}
- elif slug is not None and self.lookup_field == 'pk':
- warnings.warn(
- 'The `slug_url_kwarg` attribute is due to be deprecated. '
- 'Use the `lookup_field` attribute instead',
- PendingDeprecationWarning
- )
- filter_kwargs = {self.slug_field: slug}
- else:
- raise ImproperlyConfigured(
- 'Expected view %s to be called with a URL keyword argument '
- 'named "%s". Fix your URL conf, or set the `.lookup_field` '
- 'attribute on the view correctly.' %
- (self.__class__.__name__, self.lookup_field)
- )
-
- obj = get_object_or_404(queryset, **filter_kwargs)
-
- # May raise a permission denied
- self.check_object_permissions(self.request, obj)
-
- return obj
-
- ########################
- ### The following are placeholder methods,
- ### and are intended to be overridden.
- ###
- ### The are not called by GenericAPIView directly,
- ### but are used by the mixin methods.
-
- def pre_save(self, obj):
+ @property
+ def paginator(self):
"""
- Placeholder method for calling before saving an object.
-
- May be used to set attributes on the object that are implicit
- in either the request, or the url.
+ The paginator instance associated with the view, or `None`.
"""
- pass
-
- def post_save(self, obj, created=False):
- """
- Placeholder method for calling after saving an object.
- """
- pass
-
- def metadata(self, request):
- """
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
-
- We override the default behavior, and add some extra information
- about the required request body for POST and PUT operations.
- """
- ret = super(GenericAPIView, self).metadata(request)
-
- actions = {}
- for method in ('PUT', 'POST'):
- if method not in self.allowed_methods:
- continue
-
- cloned_request = clone_request(request, method)
- try:
- # Test global permissions
- self.check_permissions(cloned_request)
- # Test object permissions
- if method == 'PUT':
- self.get_object()
- except (exceptions.APIException, PermissionDenied, Http404):
- pass
+ if not hasattr(self, '_paginator'):
+ if self.pagination_class is None:
+ self._paginator = None
else:
- # If user has appropriate permissions for the view, include
- # appropriate metadata about the fields that should be supplied.
- serializer = self.get_serializer()
- actions[method] = serializer.metadata()
+ self._paginator = self.pagination_class()
+ return self._paginator
- if actions:
- ret['actions'] = actions
+ def paginate_queryset(self, queryset):
+ """
+ Return a single page of results, or `None` if pagination is disabled.
+ """
+ if self.paginator is None:
+ return None
+ return self.paginator.paginate_queryset(queryset, self.request, view=self)
- return ret
+ def get_paginated_response(self, data):
+ """
+ Return a paginated style `Response` object for the given output data.
+ """
+ assert self.paginator is not None
+ return self.paginator.get_paginated_response(data)
-##########################################################
-### Concrete view classes that provide method handlers ###
-### by composing the mixin classes with the base view. ###
-##########################################################
+# Concrete view classes that provide method handlers
+# by composing the mixin classes with the base view.
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
@@ -473,27 +291,3 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
-
-
-##########################
-### Deprecated classes ###
-##########################
-
-class MultipleObjectAPIView(GenericAPIView):
- def __init__(self, *args, **kwargs):
- warnings.warn(
- 'Subclassing `MultipleObjectAPIView` is due to be deprecated. '
- 'You should simply subclass `GenericAPIView` instead.',
- PendingDeprecationWarning, stacklevel=2
- )
- super(MultipleObjectAPIView, self).__init__(*args, **kwargs)
-
-
-class SingleObjectAPIView(GenericAPIView):
- def __init__(self, *args, **kwargs):
- warnings.warn(
- 'Subclassing `SingleObjectAPIView` is due to be deprecated. '
- 'You should simply subclass `GenericAPIView` instead.',
- PendingDeprecationWarning, stacklevel=2
- )
- super(SingleObjectAPIView, self).__init__(*args, **kwargs)
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.mo b/rest_framework/locale/ar/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..fe1b676c6
Binary files /dev/null and b/rest_framework/locale/ar/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.po b/rest_framework/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 000000000..a910a7c99
--- /dev/null
+++ b/rest_framework/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Eyad Toma , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Arabic (http://www.transifex.com/projects/p/django-rest-framework/language/ar/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ar\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "اسم المستخدم/كلمة السر غير صحيحين."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "المستخدم غير مفعل او تم حذفه."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "حدث خطأ في المخدم."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "بيانات الدخول غير صحيحة."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "لم يتم تزويد بيانات الدخول."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "ليس لديك صلاحية للقيام بهذا الإجراء."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "غير موجود."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "هذا الحقل مطلوب."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "لا يمكن لهذا الحقل ان يكون فارغاً null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" ليس قيمة منطقية."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "لا يمكن لهذا الحقل ان يكون فارغاً."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "تأكد ان الحقل لا يزيد عن {max_length} محرف."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "تأكد ان الحقل {min_length} محرف على الاقل."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "عليك ان تدخل بريد إلكتروني صالح."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "هذه القيمة لا تطابق النمط المطلوب."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "الرجاء إدخال رابط إلكتروني صالح."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "الرجاء إدخال رقم صحيح صالح."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "تأكد ان القيمة أقل أو تساوي {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "تأكد ان القيمة أكبر أو تساوي {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "الرجاء إدخال رقم صالح."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "تأكد ان القيمة لا تحوي أكثر من {max_digits} رقم."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة التاريخ غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" ليست واحدة من الخيارات الصالحة."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "لم يتم إرسال أي ملف."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "الملف الذي تم إرساله فارغ."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "تأكد ان اسم الملف لا يحوي أكثر من {max_length} محرف (الإسم المرسل يحوي {length} محرف)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "رقم الصفحة \"{page_number}\" غير صالح : {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "معرف العنصر \"{pk_value}\" غير صالح - العنصر غير موجود."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "قيمة غير صالحة."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "حساب المستخدم غير مفعل."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "تعذر تسجيل الدخول بالبيانات التي ادخلتها."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "يجب أن تتضمن \"اسم المستخدم\" و \"كلمة المرور\"."
diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.mo b/rest_framework/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..a5e67713a
Binary files /dev/null and b/rest_framework/locale/cs/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.po b/rest_framework/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 000000000..50e7034bd
--- /dev/null
+++ b/rest_framework/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Jirka Vejrazka , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/django-rest-framework/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Chybná hlavička. Nebyly poskytnuty přihlašovací údaje."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Chybná hlavička. Přihlašovací údaje by neměly obsahovat mezery."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Chybná hlavička. Přihlašovací údaje nebyly správně zakódovány pomocí base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Chybné uživatelské jméno nebo heslo."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Chybná hlavička tokenu. Nebyly zadány přihlašovací údaje."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Chybná hlavička tokenu. Přihlašovací údaje by neměly obsahovat mezery."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Chybný token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Uživatelský účet je neaktivní nebo byl smazán."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Chyba na straně serveru."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Neplatný formát požadavku."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Chybné přihlašovací údaje."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Přihlašovací údaje nebyly zadány."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "K této akci nemáte oprávnění."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nenalezeno."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoda \"{method}\" není povolena."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nelze vyhovět požadavku v hlavičce Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nepodporovaný media type \"{media_type}\" v požadavku."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Pořadavek byl limitován kvůli omezení počtu požadavků za časovou periodu."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Toto pole je vyžadováno."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Toto pole nesmí být prázdné (null)."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" nelze použít jako typ boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Toto pole nesmí být prázdné.."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Zkontrolujte, že toto pole není delší než {max_length} znaků."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Zkontrolujte, že toto obsahuje alespoň {min_length} znaků"
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Vložte platnou e-mailovou adresu."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Hodnota v tomto poli neodpovídá požadovanému formátu."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Vložte platnou \"zkrácenou formu\" obsahující pouze malá písmena, čísla, spojovník nebo podtržítko."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Vložte platný odkaz."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Je vyžadováno číslo."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Zkontrolujte, že hodnota je menší nebo rovna {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Zkontrolujte, že hodnota je větší nebo rovna {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Řetězec je příliš dlouhý"
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Je vyžadováno číslo."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Zkontrolujte, že číslo neobsahuje více než {max_digits} čislic."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Zkontrolujte, že číslo nemá více než {max_decimal_places} desetinných míst."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Zkontrolujte, že číslo neobsahuje více než {max_whole_digits} čislic před desetinnou čárkou."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát data a času. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Bylo zadáno pouze datum místo data a času."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát data. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Bylo zadáno datum a čas, místo samotného data."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát času. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" není platnou možností."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Byl očekáván seznam položek ale nalezen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Nebyl zaslán žádný soubor."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Zaslaná data neobsahují soubor. Zkontrolujte typ kódování ve formuláři."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nebylo možno zjistit jméno souboru."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Zaslaný soubor je prázdný."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Zajistěte, aby jméno souboru obsahovalo maximálně {max_length} znaků (teď má {length} znaků)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Nahrajte platný obrázek. Nahraný soubor buď není obrázkem, nebo je poškozen."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Chybné čislo stránky \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Chybný primární klíč \"{pk_value}\" - objekt neexistuje."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Chybný typ. Byl přijat typ {data_type} místo hodnoty primárního klíče."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Chybný odkaz - nebyla nalezena žádní shoda."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Chybný odkaz - byla nalezena neplatná shoda."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Chybný odkaz - objekt neexistuje."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Chybný typ. Byl přijat typ {data_type} místo očekávaného odkazu."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objekt s {slug_name}={value} neexistuje."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Chybná hodnota."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Chybná data. Byl přijat typ {datatype} místo očakávaného slovníku."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Tato položka musí být unikátní."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Položka {field_names} musí tvořit unikátní množinu."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Tato položka musí být pro datum \"{date_field}\" unikátní."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Tato položka musí být pro měsíc \"{date_field}\" unikátní."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Tato položka musí být pro rok \"{date_field}\" unikátní."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Chybné číslo verze v hlavičce Accept"
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Chybné číslo verze v odkazu."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Chybné číslo verze v hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Chybné čislo verze v URL parametru."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Uživatelský účet je zamčen."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Se zadanými údaji nebylo možné se přihlásit."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musí obsahovat \"uživatelské jméno! a \"heslo\"."
diff --git a/rest_framework/locale/da/LC_MESSAGES/django.mo b/rest_framework/locale/da/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..f947f9071
Binary files /dev/null and b/rest_framework/locale/da/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/da/LC_MESSAGES/django.po b/rest_framework/locale/da/LC_MESSAGES/django.po
new file mode 100644
index 000000000..e00ffadff
--- /dev/null
+++ b/rest_framework/locale/da/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mikkel Munch Mortensen <3xm@detfalskested.dk>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/django-rest-framework/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ugyldig basic header. Ingen legitimation angivet."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ugyldig basic header. Legitimationsstrenge må ikke indeholde mellemrum."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ugyldig basic header. Legitimationen er ikke base64 encoded på korrekt vis."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ugyldigt brugernavn/kodeord."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ugyldig token header."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ugyldig token header. Token-strenge må ikke indeholde mellemrum."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ugyldigt token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Inaktiv eller slettet bruger."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Der er sket en serverfejl."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Misdannet forespørgsel."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Ugyldig legitimation til autentificering."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Legitimation til autentificering blev ikke angivet."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Du har ikke lov til at udføre denne handling."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Ikke fundet."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoden \"{method}\" er ikke tilladt."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kunne ikke efterkomme forespørgslens Accept header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Forespørgslens media type, \"{media_type}\", er ikke understøttet."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Forespørgslen blev neddroslet."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Dette felt er påkrævet."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Dette felt må ikke være null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" er ikke en tilladt boolsk værdi."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Dette felt må ikke være tomt."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Tjek at dette felt ikke indeholder flere end {max_length} tegn."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Tjek at dette felt indeholder mindst {min_length} tegn."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Angiv en gyldig e-mailadresse."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Denne værdi passer ikke med det påkrævede mønster."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Indtast en gyldig \"slug\", bestående af bogstaver, tal, bund- og bindestreger."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Indtast en gyldig URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Et gyldigt heltal er påkrævet."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Tjek at værdien er mindre end eller lig med {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Tjek at værdien er større end eller lig med {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Strengværdien er for stor."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Et gyldigt tal er påkrævet."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Tjek at der ikke er flere end {max_digits} cifre i alt."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Tjek at der ikke er flere end {max_decimal_places} cifre efter kommaet."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Tjek at der ikke er flere end {max_whole_digits} cifre før kommaet."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datotid har et forkert format. Brug i stedet et af disse formater: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Forventede en datotid, men fik en dato."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Dato har et forkert format. Brug i stedet et af disse formater: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Forventede en dato men fik en datotid."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Klokkeslæt har forkert format. Brug i stedet et af disse formater: {format}. "
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" er ikke et gyldigt valg."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Forventede en liste, men fik noget af typen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ingen medsendt fil."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Det medsendte data var ikke en fil. Tjek typen af indkodning på formularen."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Filnavnet kunne ikke afgøres."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Den medsendte fil er tom."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Sørg for at filnavnet er højst {max_length} langt (det er {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Medsend et gyldigt billede. Den medsendte fil var enten ikke et billede eller billedfilen var ødelagt."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Ugyldig side \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Ugyldig primærnøgle \"{pk_value}\" - objektet findes ikke."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Ugyldig type. Forventet værdi er primærnøgle, fik {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Ugyldigt hyperlink - intet URL match."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Ugyldigt hyperlink - forkert URL match."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Ugyldigt hyperlink - objektet findes ikke."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Forkert type. Forventede en URL-streng, fik {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Object med {slug_name}={value} findes ikke."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ugyldig værdi."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ugyldig data. Forventede en dictionary, men fik {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Dette felt skal være unikt."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Felterne {field_names} skal udgøre et unikt sæt."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-datoen."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-måneden."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-året."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Ugyldig version i \"Accept\" headeren."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Ugyldig version i URL-stien."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Ugyldig version i hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Ugyldig version i forespørgselsparameteren."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Brugerkontoen er deaktiveret."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kunne ikke logge ind med den angivne legitimation."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Skal indeholde \"username\" og \"password\"."
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.mo b/rest_framework/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..48245c609
Binary files /dev/null and b/rest_framework/locale/de/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.po b/rest_framework/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 000000000..74bee4167
--- /dev/null
+++ b/rest_framework/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Thomas Tanner, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: German (http://www.transifex.com/projects/p/django-rest-framework/language/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ungültiger basic header. Keine Zugangsdaten angegeben."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ungültiger basic header. Zugangsdaten sollen keine Leerzeichen enthalten."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ungültiger basic header. Zugangsdaten sind nicht korrekt mit base64 kodiert."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ungültiger Benutzername/Passwort"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ungültiger token header. Keine Zugangsdaten angegeben."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ungültiger token header. Zugangsdaten sollen keine Leerzeichen enthalten."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ungültiges Token"
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Benutzer inaktiv oder gelöscht."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ein Serverfehler ist aufgetreten."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Fehlerhafte Anfrage."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Falsche Anmeldedaten."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Anmeldedaten fehlen."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Sie sind nicht berechtigt, diese Aktion durchzuführen."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nicht gefunden."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Methode \"{method}\" nicht erlaubt."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kann den Accept header der Anfrage nicht erfüllen."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nicht unterstützter Medientyp \"{media_type}\" in der Anfrage."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Die Anfrage wurde gedrosselt."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Dieses Feld ist erforderlich."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Dieses Feld darf nicht Null sein."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" ist kein gültiger Boole'scher Wert."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Dieses Feld darf nicht leer sein."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Stelle sicher, dass dieses Feld nicht mehr als {max_length} Zeichen lang ist."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Stelle sicher, dass dieses Feld mindestens {min_length} Zeichen lang ist."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Gebe eine gültige E-Mail Adresse an."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Dieser Wert passt nicht zu dem erforderlichen Muster."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Gebe ein gültiges \"slug\" aus Buchstaben, Ziffern, Unterstrichen und Minuszeichen ein."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Gebe eine gültige URL ein."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Eine gültige Ganzzahl ist erforderlich."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Stelle sicher, dass dieser Wert kleiner oder gleich {max_value} ist."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Stelle sicher, dass dieser Wert größer oder gleich {max_value} ist."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Zeichenkette zu lang."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Eine gültige Zahl ist erforderlich."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Stelle sicher, dass es insgesamt nicht mehr als {max_digits} Ziffern lang ist."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Stelle sicher, dass es nicht mehr als {max_decimal_places} Nachkommastellen lang ist."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Stelle sicher, dass es nicht mehr als {max_whole_places} Stellen vor dem Komma lang ist."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datum- und Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Erwarte eine Datum- und Zeitangabe, erhielt aber ein Datum."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Datum hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Erwarte ein Datum, erhielt aber eine Datum- und Zeitangabe."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" ist keine gültige Option."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Erwarte eine Liste von Elementen, erhielt aber den Typ \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Es wurde keine Datei übermittelt."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Die übermittelten Daten sind keine Datei. Prüfe den Kodierungstyp im Formular."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Der Dateiname konnte nicht ermittelt werden."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Die übermittelte Datei ist leer."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ungültiger Wert."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ungültige Daten. Dictionary erwartet, aber {datatype} erhalten."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Dieses Feld muss eineindeutig sein."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Benutzerkonto ist gesperrt."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"username\" und \"password\" sind erforderlich."
diff --git a/rest_framework/locale/en/LC_MESSAGES/django.mo b/rest_framework/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..746915ff1
Binary files /dev/null and b/rest_framework/locale/en/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/en/LC_MESSAGES/django.po b/rest_framework/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 000000000..f3db69e5e
--- /dev/null
+++ b/rest_framework/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: English (http://www.transifex.com/projects/p/django-rest-framework/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Invalid basic header. No credentials provided."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Invalid basic header. Credentials string should not contain spaces."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Invalid basic header. Credentials not correctly base64 encoded."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Invalid username/password."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Invalid token header. No credentials provided."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Invalid token header. Token string should not contain spaces."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Invalid token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "User inactive or deleted."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "A server error occurred."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Malformed request."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Incorrect authentication credentials."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Authentication credentials were not provided."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "You do not have permission to perform this action."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Not found."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Method \"{method}\" not allowed."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Could not satisfy the request Accept header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Unsupported media type \"{media_type}\" in request."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Request was throttled."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "This field is required."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "This field may not be null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" is not a valid boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "This field may not be blank."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Ensure this field has no more than {max_length} characters."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Ensure this field has at least {min_length} characters."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Enter a valid email address."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "This value does not match the required pattern."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Enter a valid \"slug\" consisting of letters, numbers, underscores or hyphens."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Enter a valid URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr "\"{value}\" is not a valid UUID."
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "A valid integer is required."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Ensure this value is less than or equal to {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Ensure this value is greater than or equal to {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "String value too large."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "A valid number is required."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Ensure that there are no more than {max_digits} digits in total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Ensure that there are no more than {max_decimal_places} decimal places."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Ensure that there are no more than {max_whole_digits} digits before the decimal point."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Expected a datetime but got a date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Date has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Expected a date but got a datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" is not a valid choice."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Expected a list of items but got type \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "No file was submitted."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "The submitted data was not a file. Check the encoding type on the form."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "No filename could be determined."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "The submitted file is empty."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Ensure this filename has at most {max_length} characters (it has {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Upload a valid image. The file you uploaded was either not an image or a corrupted image."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr "Expected a dictionary of items but got type \"{input_type}\"."
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Invalid page \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr "Invalid cursor"
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Invalid pk \"{pk_value}\" - object does not exist."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Incorrect type. Expected pk value, received {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Invalid hyperlink - No URL match."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Invalid hyperlink - Incorrect URL match."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Invalid hyperlink - Object does not exist."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Incorrect type. Expected URL string, received {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Object with {slug_name}={value} does not exist."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Invalid value."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Invalid data. Expected a dictionary, but got {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "This field must be unique."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "The fields {field_names} must make a unique set."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "This field must be unique for the \"{date_field}\" date."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "This field must be unique for the \"{date_field}\" month."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "This field must be unique for the \"{date_field}\" year."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Invalid version in \"Accept\" header."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Invalid version in URL path."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Invalid version in hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Invalid version in query parameter."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "User account is disabled."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Unable to log in with provided credentials."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Must include \"username\" and \"password\"."
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.mo b/rest_framework/locale/en_US/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..eb60d9d7e
Binary files /dev/null and b/rest_framework/locale/en_US/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
new file mode 100644
index 000000000..11d94e9ca
--- /dev/null
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR , YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:40+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME \n"
+"Language-Team: LANGUAGE \n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93 views.py:77
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid "Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid "The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: views.py:81
+msgid "Permission denied."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/es/LC_MESSAGES/django.mo b/rest_framework/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..814db7be9
Binary files /dev/null and b/rest_framework/locale/es/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/es/LC_MESSAGES/django.po b/rest_framework/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 000000000..28ef893d4
--- /dev/null
+++ b/rest_framework/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,327 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# José Padilla , 2015
+# Miguel González , 2015
+# Sergio Infante , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/django-rest-framework/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Cabecera básica inválida. Las credenciales no fueron suministradas."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Cabecera básica inválida. La cadena con las credenciales no debe contener espacios."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Cabecera básica inválida. Las credenciales incorrectamente codificadas en base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nombre de usuario/contraseña inválidos."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Cabecera token inválida. Las credenciales no fueron suministradas."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Cabecera token inválida. La cadena token no debe contener espacios."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token inválido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Usuario inactivo o borrado."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Se ha producido un error en el servidor."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Solicitud con formato incorrecto."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenciales de autenticación incorrectas."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Las credenciales de autenticación no se proveyeron."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Usted no tiene permiso para realizar esta acción."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "No encontrado."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Método \"{method}\" no permitido."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "No se ha podido satisfacer la solicitud de cabecera de Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Tipo de medio \"{media_type}\" incompatible en la solicitud."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Solicitud fue regulada (throttled)."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Este campo es requerido."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Este campo no puede ser nulo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" no es un booleano válido."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Este campo no puede estar en blanco."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Asegúrese de que este campo no tenga más de {max_length} caracteres."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Asegúrese de que este campo tenga al menos {min_length} caracteres."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Introduzca una dirección de correo electrónico válida."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Este valor no coincide con el patrón requerido."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Introduzca un \"slug\" válido consistente en letras, números, guiones o guiones bajos."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Introduzca una URL válida."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Introduzca un número entero válido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Asegúrese de que este valor es menor o igual a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Asegúrese de que este valor es mayor o igual a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Cadena demasiado larga."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Se requiere un número válido."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Asegúrese de que no haya más de {max_digits} dígitos en total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Asegúrese de que no haya más de {max_decimal_places} decimales."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Asegúrese de que no haya más de {max_whole_digits} dígitos en la parte entera."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Fecha/hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Se esperaba un fecha/hora en vez de una fecha."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Fecha con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Se esperaba una fecha en vez de una fecha/hora."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" no es una elección válida."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Se esperaba una lista de elementos en vez del tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "No se envió ningún archivo."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "La información enviada no era un archivo. Compruebe el tipo de codificación del formulario."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "No se pudo determinar un nombre de archivo."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "El archivo enviado está vació."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Asegúrese de que el nombre de archivo no tenga más de {max_length} caracteres (tiene {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Adjunte una imagen válida. El archivo adjunto o bien no es una imagen o bien está dañado."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Página \"{page_number}\" inválida: {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Clave primaria \"{pk_value}\" inválida - objeto no existe."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Tipo incorrecto. Se esperaba valor de clave primaria y se recibió {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Hiperenlace inválido - No hay URL coincidentes."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Hiperenlace inválido - Coincidencia incorrecta de la URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Hiperenlace inválido - Objeto no existe."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Tipo incorrecto. Se esperaba una URL y se recibió {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objeto con {slug_name}={value} no existe."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valor inválido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Datos inválidos. Se esperaba un diccionario pero es un {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Este campo debe ser único."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Los campos {field_names} deben formar un conjunto único."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Este campo debe ser único para el día \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Este campo debe ser único para el mes \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Este campo debe ser único para el año \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Versión inválida en la cabecera \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Versión inválida en la ruta de la URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Versión inválida en el nombre de host."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Versión inválida en el parámetro de consulta."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Cuenta de usuario está deshabilitada."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "No puede iniciar sesión con las credenciales proporcionadas."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Debe incluir \"username\" y \"password\"."
diff --git a/rest_framework/locale/et/LC_MESSAGES/django.mo b/rest_framework/locale/et/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..ca9b6ec4b
Binary files /dev/null and b/rest_framework/locale/et/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/et/LC_MESSAGES/django.po b/rest_framework/locale/et/LC_MESSAGES/django.po
new file mode 100644
index 000000000..dec03d4d8
--- /dev/null
+++ b/rest_framework/locale/et/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Tõnis Kärdi , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Estonian (http://www.transifex.com/projects/p/django-rest-framework/language/et/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Vale kasutajatunnus/salasõna."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Kasutaja on inaktiivne või kustutatud."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Väli on kohustuslik."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Väli ei tohi olla tühi."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Sisesta kehtiv e-posti aadress."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Väärtus ei ühti etteantud mustriga."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Sisesta korrektne URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Veendu, et väärtus on väiksem kui või võrdne väärtusega {max_value}. "
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Veendu, et väärtus on suurem kui või võrdne väärtusega {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Sõne on liiga pikk."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Veendu, et kokku pole rohkem kui {max_digits}."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Veendu, et komakohti pole rohkem kui {max_decimal_places}. "
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Kasutajakonto on suletud"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Sisselogimine antud tunnusega ebaõnnestus."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Peab sisaldama \"kasutajatunnust\" ja \"slasõna\"."
diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.mo b/rest_framework/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..68519d453
Binary files /dev/null and b/rest_framework/locale/fr/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.po b/rest_framework/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 000000000..e8597c305
--- /dev/null
+++ b/rest_framework/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Etienne Desgagné , 2015
+# Martin Maillard , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: French (http://www.transifex.com/projects/p/django-rest-framework/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "En-tête « basic » non valide. Informations d'identification non fournies."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "En-tête « basic » non valide. Les informations d'identification ne doivent pas contenir d'espaces."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "En-tête « basic » non valide. Encodage base64 des informations d'identification incorrect."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nom d'utilisateur et/ou mot de passe non valide(s)."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "En-tête « token » non valide. Informations d'identification non fournies."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "En-tête « token » non valide. Un token ne doit pas contenir d'espaces."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token non valide."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Utilisateur inactif ou supprimé."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Une erreur du serveur est survenue."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Requête malformée"
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Informations d'authentification incorrectes."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Informations d'authentification non fournies."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Vous n'avez pas la permission d'effectuer cette action."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Méthode \"{method}\" non autorisée."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ce champ est obligatoire."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ce champ ne peut être null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" n'est pas un booléen valide."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ce champ ne peut être vide."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Assurez-vous que ce champ comporte au plus {max_length} caractères."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Assurez-vous que ce champ comporte au moins {min_length} caractères."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Saisissez une adresse email valable."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Cette valeur ne satisfait pas le motif imposé."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Ce champ ne doit contenir que des lettres, des nombres, des tirets bas _ et des traits d'union."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Saisissez une URL valide."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Saisissez un nombre entier valide."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Assurez-vous que cette valeur est inférieure ou égale à {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Assurez-vous que cette valeur est supérieure ou égale à {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Chaîne de caractères trop longue."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Un nombre valide est requis."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_digits} chiffres au total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_decimal_places} chiffres après la virgule."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_whole_digits} chiffres avant la virgule."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" n'est pas un choix valide."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Aucun fichier n'a été soumis."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "La donnée soumise n'est pas un fichier. Vérifiez le type d'encodage du formulaire."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Le nom de fichier n'a pu être déterminé."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Le fichier soumis est vide."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Assurez-vous que le nom de fichier comporte au plus {max_length} caractères (il en comporte {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Page \"{page_number}\" non valide : {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Clé primaire \"{pk_value}\" non valide - l'objet n'existe pas."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "L'object avec {slug_name}={value} n'existe pas."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valeur non valide."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ce champ doit être unique."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Les champs {field_names} doivent former un ensemble unique."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Ce champ doit être unique pour la date \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Ce champ doit être unique pour le mois \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Ce champ doit être unique pour l'année \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Version non valide dans l'en-tête « Accept »."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Version non valide dans l'URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Version non valide dans le nom d'hôte."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Version non valide dans le paramètre de requête."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Ce compte est désactivé."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossible de se connecter avec les informations d'identification fournies."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"username\" et \"password\" doivent être inclus."
diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.mo b/rest_framework/locale/hu/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..451b0b9ad
Binary files /dev/null and b/rest_framework/locale/hu/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.po b/rest_framework/locale/hu/LC_MESSAGES/django.po
new file mode 100644
index 000000000..14fb6544d
--- /dev/null
+++ b/rest_framework/locale/hu/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Zoltan Szalai , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Hungarian (http://www.transifex.com/projects/p/django-rest-framework/language/hu/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Érvénytelen basic fejlécmező. Nem voltak megadva azonosítók."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Érvénytelen basic fejlécmező. Az azonosító karakterlánc nem tartalmazhat szóközöket."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Érvénytelen basic fejlécmező. Az azonosítók base64 kódolása nem megfelelő."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Érvénytelen felhasználónév/jelszó."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Érvénytelen token fejlécmező. Nem voltak megadva azonosítók."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Érvénytelen token fejlécmező. A token karakterlánc nem tartalmazhat szóközöket."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Érvénytelen token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "A felhasználó nincs aktiválva vagy törölve lett."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Szerver oldali hiba történt."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Hibás kérés."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Hibás azonosítók."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Nem voltak megadva azonosítók."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Nincs jogosultsága a művelet végrehajtásához."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nem található."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "A \"{method}\" metódus nem megengedett."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "A kérés Accept fejlécmezőjét nem lehetett kiszolgálni."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nem támogatott média típus \"{media_type}\" a kérésben."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "A kérés korlátozva lett."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ennek a mezőnek a megadása kötelező."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ez a mező nem lehet null értékű."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "Az \"{input}\" nem egy érvényes logikai érték."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ez a mező nem lehet üres."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Bizonyosodjon meg arról, hogy ez a mező legfeljebb {max_length} karakterből áll."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Bizonyosodjon meg arról, hogy ez a mező legalább {min_length} karakterből áll."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Adjon meg egy érvényes e-mail címet!"
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ez az érték nem illeszkedik a szükséges mintázatra."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Az URL barát cím csak betűket, számokat, aláhúzásokat és kötőjeleket tartalmazhat."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Adjon meg egy érvényes URL-t!"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Egy érvényes egész szám megadása szükséges."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Bizonyosodjon meg arról, hogy ez az érték legfeljebb {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Bizonyosodjon meg arról, hogy ez az érték legalább {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "A karakterlánc túl hosszú."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Egy érvényes szám megadása szükséges."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Bizonyosodjon meg arról, hogy a számjegyek száma összesen legfeljebb {max_digits}."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Bizonyosodjon meg arról, hogy a tizedes tört törtrészében levő számjegyek száma összesen legfeljebb {max_decimal_places}."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Bizonyosodjon meg arról, hogy a tizedes tört egész részében levő számjegyek száma összesen legfeljebb {max_whole_digits}."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Időt is tartalmazó dátum helyett egy időt nem tartalmazó dátum lett elküldve."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Időt nem tartalmazó dátum helyett egy időt is tartalmazó dátum lett elküldve."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Az idő formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "Az \"{input}\" nem egy érvényes elem."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Elemek listája helyett \"{input_type}\" lett elküldve."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Semmilyen fájl sem került feltöltésre."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Az elküldött adat nem egy fájl volt. Ellenőrizze a kódolás típusát az űrlapon!"
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "A fájlnév nem megállapítható."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "A küldött fájl üres."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Bizonyosodjon meg arról, hogy a fájlnév legfeljebb {max_length} karakterből áll (jelenlegi hossza: {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Töltsön fel egy érvényes képfájlt! A feltöltött fájl nem kép volt, vagy megsérült."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Érvénytelen oldal \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Érvénytelen pk \"{pk_value}\" - az objektum nem létezik."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Helytelen típus. pk érték helyett {data_type} lett elküldve."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Érvénytelen link - Nem illeszkedő URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Érvénytelen link. - Eltérő URL illeszkedés."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Érvénytelen link - Az objektum nem létezik."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Helytelen típus. URL karakterlánc helyett {data_type} lett elküldve."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Nem létezik olyan objektum, amelynél {slug_name}={value}."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Érvénytelen érték."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Érvénytelen adat. Egy dictionary helyett {datatype} lett elküldve."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ennek a mezőnek egyedinek kell lennie."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "A {field_names} mezőnevek nem tartalmazhatnak duplikátumot."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" dátumra."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" hónapra."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" évre."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Érvénytelen verzió az \"Accept\" fejlécmezőben."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Érvénytelen verzió az URL elérési útban."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Érvénytelen verzió a hosztnévben."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Érvénytelen verzió a lekérdezési paraméterben."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "A felhasználó tiltva van."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "A megadott azonosítókkal nem lehet bejelentkezni."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Tartalmaznia kell a \"felhasználónevet\" és a \"jelszót\"."
diff --git a/rest_framework/locale/id/LC_MESSAGES/django.mo b/rest_framework/locale/id/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..7fc98bdaa
Binary files /dev/null and b/rest_framework/locale/id/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/id/LC_MESSAGES/django.po b/rest_framework/locale/id/LC_MESSAGES/django.po
new file mode 100644
index 000000000..99b705467
--- /dev/null
+++ b/rest_framework/locale/id/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Indonesian (http://www.transifex.com/projects/p/django-rest-framework/language/id/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: id\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/it/LC_MESSAGES/django.mo b/rest_framework/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..82ceb810e
Binary files /dev/null and b/rest_framework/locale/it/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/it/LC_MESSAGES/django.po b/rest_framework/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 000000000..2cdfb8779
--- /dev/null
+++ b/rest_framework/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mattia Procopio , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Italian (http://www.transifex.com/projects/p/django-rest-framework/language/it/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nome utente/password non validi"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Header del token non valido. Credenziali non fornite."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Header del token non valido. Il contenuto del token non dovrebbe contenere spazi."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token invalido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Utente inattivo o eliminato."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Errore del server."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Richiesta malformata."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenziali di autenticazione incorrette."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Non sono state immesse le credenziali di autenticazione."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Non hai l'autorizzazione per eseguire questa azione."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Non trovato."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metodo \"{method}\" non consentito"
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Impossibile soddisfare l'header \"Accept\" presente nella richiesta."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Tipo di media \"{media_type}\"non supportato."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Campo obbligatorio."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Il campo non puà essere nullo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" non è un valido valore booleano."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Questo campo non può essere omesso."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Assicurati che questo campo non abbia più di {max_length} caratteri."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Assicurati che questo campo abbia almeno {max_length} caratteri."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Inserisci un indirizzo email valido."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Immetti uno \"slug\" valido che consista di lettere, numeri, underscore o trattini."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Inserisci un URL valido"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "È richiesto un numero intero valido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Assicurati che il valore sia minore o uguale a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Assicurati che il valore sia maggiore o uguale a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "È richiesto un numero valido."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Assicurati che non ci siano più di {max_digits} cifre in totale."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Assicurati che non ci siano più di {max_decimal_places} cifre decimali."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Assicurati che non ci siano più di {max_whole_digits} cifre prima del separatore decimale."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "L'oggetto di tipo datetime è in un formato errato. Usa uno dei seguenti formati: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Atteso un oggetto di tipo datetime ma l'oggetto ricevuto è di tipo date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "La data è in un formato errato. Usa uno dei seguenti formati: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Atteso un oggetto di tipo date ma l'oggetto ricevuto è di tipo datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" non è una scelta valida."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Attesa una lista di oggetti ma l'oggetto ricevuto è di tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Non è stato inviato alcun file."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Il nome del file non può essere determinato."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Il file inviato è vuoto."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valore non valido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Questo campo deve essere unico."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "L'account dell'utente è disabilitato"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossibile eseguire il log in con le credenziali immesse."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Deve includere \"nome utente\" e \"password\"."
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..457bb53c2
Binary files /dev/null and b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
new file mode 100644
index 000000000..963fe89a5
--- /dev/null
+++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# SUN CHOI , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/django-rest-framework/language/ko_KR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko_KR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials) 문자열은 빈칸(spaces)을 포함하지 않아야 합니다."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 base64로 적절히 부호화(encode)되지 않았습니다."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "아이디/비밀번호가 유효하지 않습니다."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "토큰 헤더가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 빈칸(spaces)를 포함하지 않아야 합니다."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "토큰이 유효하지 않습니다."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "계정이 중지되었거나 삭제되었습니다."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "서버 장애가 발생했습니다."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "잘못된 요청입니다."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "자격 인증데이터(authentication credentials)가 정확하지 않습니다."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "이 작업을 "
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "찾을 수 없습니다."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "메소드(Method) \"{method}\"는 허용되지 않습니다."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "요청된 \"{media_type}\"가 지원되지 않는 미디어 형태입니다."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "요청이 지연(throttled)되었습니다."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "이 항목을 채워주십시오."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\"이 유효하지 않은 부울(boolean)입니다."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "이 칸이 글자 수가 {max_length} 이하인지 확인하십시오."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "이 칸이 글자 수가 적어도 {min_length} 이상인지 확인하십시오."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "유효한 이메일 주소를 입력하십시오."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "형식에 맞지 않는 값입니다."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하십시오."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "유효한 URL을 입력하십시오."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "유효한 정수(integer)를 넣어주세요."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "이 값이 {max_value}보다 작거나 같은지 확인하십시오."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "이 값이 {min_value}보다 크거나 같은지 확인하십시오."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "문자열 값이 너무 큽니다."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "유효한 숫자를 넣어주세요."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "전체 숫자(digits)가 {max_digits} 이하인지 확인하십시오."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "소수점 자릿수가 {max_decimal_places} 이하인지 확인하십시오."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "소수점 자리 앞에 숫자(digits)가 {max_whole_digits} 이하인지 확인하십시오."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "예상된 datatime 대신 date를 받았습니다."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Date의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "예상된 date 대신 datetime을 받았습니다."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\"이 유효하지 않은 선택(choice)입니다."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "아이템 리스트가 예상되었으나 \"{input_type}\"를 받았습니다."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "파일이 제출되지 않았습니다."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "제출된 데이터는 파일이 아닙니다. 제출된 서식의 인코딩 형식을 확인하세요."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "파일명을 알 수 없습니다."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "제출된 파일이 비어있습니다."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "이 파일명의 글자수가 최대 {max_length}를 넘지 않는지 확인하십시오. (이것은 {length}가 있습니다)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "유효한 이미지 파일을 업로드 하십시오. 업로드 하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "유효하지 않은 page \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "유효하지 않은 pk \"{pk_value}\" - 객체가 존재하지 않습니다."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "잘못된 형식입니다. pk 값 대신 {data_type}를 받았습니다."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "유효하지 않은 하이퍼링크 - 일치하는 URL이 없습니다."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "유효하지 않은 하이퍼링크 - URL이 일치하지 않습니다."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "유효하지 않은 하이퍼링크 - 객체가 존재하지 않습니다."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "잘못된 형식입니다. URL 문자열을 예상했으나 {data_type}을 받았습니다."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "{slug_name}={value} 객체가 존재하지 않습니다."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "값이 유효하지 않습니다."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "유효하지 않은 데이터. 딕셔너리(dictionary)대신 {datatype}를 받았습니다."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "사용자 계정을 사용할 수 없습니다."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "제공된 인증데이터(credentials)로는 로그인할 수 없습니다."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"아이디\"와 \"비밀번호\"를 포함해야 합니다."
diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.mo b/rest_framework/locale/mk/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..fc5866269
Binary files /dev/null and b/rest_framework/locale/mk/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.po b/rest_framework/locale/mk/LC_MESSAGES/django.po
new file mode 100644
index 000000000..d9a469531
--- /dev/null
+++ b/rest_framework/locale/mk/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Filip Dimitrovski , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Macedonian (http://www.transifex.com/projects/p/django-rest-framework/language/mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Невалиден основен header. Не се внесени податоци за автентикација."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Невалиден основен header. Автентикационата низа не треба да содржи празни места."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Невалиден основен header. Податоците за автентикација не се енкодирани со base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Невалидно корисничко име/лозинка."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Невалиден токен header. Не се внесени податоци за најава."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Невалиден токен во header. Токенот не треба да содржи празни места."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Невалиден токен."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Корисникот е деактивиран или избришан."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Настана серверска грешка."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Неправилен request."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Неточни податоци за најава."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Не се внесени податоци за најава."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Немате дозвола да го сторите ова."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Не е пронајдено ништо."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Методата \"{method}\" не е дозволена."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Не може да се исполни барањето на Accept header-от."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Media типот „{media_type}“ не е поддржан."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Request-от е забранет заради ограничувања."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ова поле е задолжително."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ова поле не смее да биде недефинирано."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" не е валиден boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ова поле не смее да биде празно."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Ова поле не смее да има повеќе од {max_length} знаци."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Ова поле мора да има барем {min_length} знаци."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Внесете валидна email адреса."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ова поле не е по правилната шема/барање."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Внесете валидно име што содржи букви, бројки, долни црти или црти."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Внесете валиден URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Задолжителен е валиден цел број."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Вредноста треба да биде помала или еднаква на {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Вредноста треба да биде поголема или еднаква на {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Вредноста е преголема."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Задолжителен е валиден број."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Не смее да има повеќе од {max_digits} цифри вкупно."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Не смее да има повеќе од {max_decimal_places} децимални места."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Не смее да има повеќе од {max_whole_digits} цифри пред децималната точка."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Датата и времето се со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Очекувано беше дата и време, а внесено беше само дата."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Датата е со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Очекувана беше дата, а внесени беа и дата и време."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Времето е со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "„{input}“ не е валиден избор."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Очекувана беше листа, а внесено беше „{input_type}“."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ниеден фајл не е качен (upload-иран)."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Испратените податоци не се фајл. Проверете го encoding-от на формата."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Не може да се открие име на фајлот."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Качениот (upload-иран) фајл е празен."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Името на фајлот треба да има највеќе {max_length} знаци (а има {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Качете (upload-ирајте) валидна слика. Фајлот што го качивте не е валидна слика или е расипан."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Невалидна страна „{page_number}“: {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Невалиден pk „{pk_value}“ - објектот не постои."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Неточен тип. Очекувано беше pk, а внесено {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Невалиден хиперлинк - не е внесен URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Невалиден хиперлинк - внесен е неправилен URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Невалиден хиперлинк - Објектот не постои."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Неточен тип. Очекувано беше URL, a внесено {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Објектот со {slug_name}={value} не постои."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Невалидна вредност."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Невалидни податоци. Очекуван беше dictionary, а внесен {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ова поле мора да биде уникатно."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Полињата {field_names} заедно мора да формираат уникатен збир."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ датата."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ месецот."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ годината."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Невалидна верзија во „Accept“ header-от."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Невалидна верзија во URL патеката."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Невалидна верзија во hostname-от."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Невалидна верзија во query параметарот."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Сметката на корисникот е деактивирана."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Не може да се најавите со податоците за најава."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Мора да се внесе „username“ и „password“."
diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.mo b/rest_framework/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..b0e1ad77f
Binary files /dev/null and b/rest_framework/locale/nl/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.po b/rest_framework/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 000000000..a12155124
--- /dev/null
+++ b/rest_framework/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/django-rest-framework/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.mo b/rest_framework/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..9db72cfb3
Binary files /dev/null and b/rest_framework/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.po b/rest_framework/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 000000000..8e51d7545
--- /dev/null
+++ b/rest_framework/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Janusz Harkot , 2015
+# Maciek Olko , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Polish (http://www.transifex.com/projects/p/django-rest-framework/language/pl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Niepoprawny podstawowy nagłówek. Brak danych uwierzytelniających."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Niepoprawny podstawowy nagłówek. Ciąg znaków danych uwierzytelniających nie powinien zawierać spacji."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Niepoprawny podstawowy nagłówek. Niewłaściwe kodowanie base64 danych uwierzytelniających."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Niepoprawna nazwa użytkownika lub hasło."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Niepoprawny nagłówek tokena. Brak danych uwierzytelniających."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Niepoprawny nagłówek tokena. Token nie może zawierać odstępów."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Niepoprawny token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Użytkownik nieaktywny lub usunięty."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Wystąpił błąd serwera."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Zniekształcone żądanie."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Błędne dane uwierzytelniające."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Nie podano danych uwierzytelniających."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Nie masz uprawnień, by wykonać tę czynność."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nie znaleziono."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Niedozwolona metoda \"{method}\"."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nie można zaspokoić nagłówka Accept żądania."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Brak wsparcia dla żądanego typu danych \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Żądanie zostało zdławione."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "To pole jest wymagane."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Pole nie może mieć wartości null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" nie jest poprawną wartością logiczną."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "To pole nie może być puste."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Upewnij się, że to pole ma nie więcej niż {max_length} znaków."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Upewnij się, że pole ma co najmniej {min_length} znaków."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Podaj poprawny adres e-mail."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ta wartość nie pasuje do wymaganego wzorca."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Wprowadź poprawną wartość pola typu \"slug\", składającą się ze znaków łacińskich, cyfr, podkreślenia lub myślnika."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Wprowadź poprawny adres URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Wymagana poprawna liczba całkowita."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Upewnij się, że ta wartość jest mniejsza lub równa {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Upewnij się, że ta wartość jest większa lub równa {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Za długi ciąg znaków."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Wymagana poprawna liczba."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_digits} cyfr."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_decimal_places} cyfr dziesiętnych."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_whole_digits} cyfr całkowitych."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Wartość daty z czasem ma zły format. Użyj jednego z dostępnych formatów: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Oczekiwano datę z czasem, otrzymano tylko datę."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Data ma zły format. Użyj jednego z tych formatów: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Oczekiwano daty a otrzymano datę z czasem."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Błędny format czasu. Użyj jednego z dostępnych formatów: {format}"
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" nie jest poprawnym wyborem."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Oczekiwano listy elementów, a otrzymano dane typu \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Nie przesłano pliku."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Przesłane dane nie były plikiem. Sprawdź typ kodowania formatki."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nie można określić nazwy pliku."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Przesłany plik jest pusty."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Upewnij się, że nazwa pliku ma długość co najwyżej {max_length} znaków (aktualnie ma {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Prześlij poprawny plik graficzny. Przesłany plik albo nie jest grafiką lub jest uszkodzony."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Niepoprawna strona \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Błędny klucz główny \"{pk_value}\" - obiekt nie istnieje."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Błędny typ danych. Oczekiwano wartość klucza głównego, otrzymano {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Błędny hyperlink - nie znaleziono pasującego adresu URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Błędny hyperlink - błędne dopasowanie adresu URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Błędny hyperlink - obiekt nie istnieje."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Błędny typ danych. Oczekiwano adresu URL, otrzymano {data_type}"
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Obiekt z polem {slug_name}={value} nie istnieje"
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Niepoprawna wartość."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Niepoprawne dane. Oczekiwano słownika, otrzymano {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Wartość dla tego pola musi być unikalna."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Pola {field_names} muszą tworzyć unikalny zestaw."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "To pole musi mieć unikalną wartość dla jednej daty z pola \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "To pole musi mieć unikalną wartość dla konkretnego miesiąca z pola \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "To pole musi mieć unikalną wartość dla konkretnego roku z pola \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Błędna wersja w nagłówku \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Błędna wersja w ścieżce URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Błędna wersja w nazwie hosta."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Błędna wersja w parametrach zapytania."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Konto użytkownika jest nieaktywne."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Podane dane uwierzytelniające nie pozwalają na zalogowanie."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musi zawierać \"username\" i \"password\"."
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..49f019294
Binary files /dev/null and b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 000000000..3f272f714
--- /dev/null
+++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Craig Blaszczyk , 2015
+# Filipe Rinaldi , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-rest-framework/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Cabeçalho básico inválido. Credenciais não fornecidas."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Cabeçalho básico inválido. String de credenciais não deve incluir espaços."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Cabeçalho básico inválido. Credenciais codificadas em base64 incorretamente."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Usário ou senha inválido."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Cabeçalho de token inválido. Credenciais não fornecidas."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Cabeçalho de token inválido. String de token não deve incluir espaços."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token inválido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Usuário inativo ou removido."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ocorreu um erro de servidor."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Pedido malformado."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenciais de autenticação incorretas."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "As credenciais de autenticação não foram fornecidas."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Você não tem persmissao para executar essa ação."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Não encontrado."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Método \"{method}\" não é permitido."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Não foi possível satisfazer a requisição do cabeçalho Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Media type \"{media_type}\" no pedido não é suportado."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Pedido foi limitado."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Este campo é obrigatório."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Este campo não pode ser nulo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" não é um valor boleano válido."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Este campo não pode ser em branco."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Certifique-se de que este campo não tenha mais de {max_length} caracteres."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Certifique-se de que este campo tenha mais de {min_length} caracteres."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Insira um endereço de email válido."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Este valor não corresponde ao padrão exigido."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados ou hífens."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Entrar um URL válido."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Um número inteiro válido é exigido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Certifique-se de que este valor seja inferior ou igual a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Certifque-se de que este valor seja maior ou igual a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Valor da string é muito grande."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Um número válido é necessário."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Certifique-se de que não haja mais de {max_digits} dígitos no total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Certifique-se de que não haja mais de {max_decimal_places} casas decimais."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Certifique-se de que não haja mais de {max_whole_digits} dígitos antes do ponto decimal."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Formato inválido para data e hora. Use um dos formatos a seguir: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Data e hora são necessários mas apenas data foi encontrada."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Formato inválido para data. Use um dos formatos a seguir: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Necessário uma data mas recebeu uma data e hora."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Tempo tem formato errado. Usa um desses em vez disso: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" não é um escolha válido."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ficheiro não foi submetido."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Os dados submetidos nao foram um ficheiro. Certifique-se do tipo de codificação no formulário."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nome do arquivo não pode ser determinado."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "O arquivo submetido ésta vázio."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Certifique-se de que o nome do ficheiro tem menos de {max_length} caracteres (tem {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Fazer upload de um imagem válido. O arquivo mandou não foi um imagem ou foi corrupto."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Página inválido \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Pk inválido \"{pk_value}\" - objeto não existe."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Tipo incorreto. Necessário valor pk, recebeu {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Hyperlink inválido - URL não combinou."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Hyperlink inválido - URL combinou errado."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Hyperlink inválido - objeto não existe."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Tipo incorreto. Necessário string URL, recebeu {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objeto com {slug_name}={value} não existe."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valor inválido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Data inválido. Necessário um dicionário mas recebeu {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Esse campo deve ser unico."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Os campos {field_names} devem criar um set unico."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "O campo deve ser unico pela data \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "O campo deve ser unico pelo anô \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "O campo deve ser unico pela mês \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Versão inválido no cabeçalho \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Versão inválido no caminho de URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Versão inválido no hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Versão inválida no parâmetro de query."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Conta de usário desabilitada."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossível fazer login com as credenciais fornecidas."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Obrigatório incluir \"usuário\" e \"senha\"."
diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.mo b/rest_framework/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..d1555f1fb
Binary files /dev/null and b/rest_framework/locale/ru/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.po b/rest_framework/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 000000000..38489747b
--- /dev/null
+++ b/rest_framework/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mikhail Dmitriev , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Russian (http://www.transifex.com/projects/p/django-rest-framework/language/ru/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Недопустимый заголовок. Не предоставлены учетные данные."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Недопустимый заголовок. Учетные данные не должны содержать пробелов."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Недопустимый заголовок. Учетные данные некорректно закодированны в base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Недопустимые имя пользователя или пароль."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Недопустимый заголовок токена. Не предоставлены учетные данные."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Недопустимый заголовок токена. Токен не должен содержать пробелов."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Недопустимый токен."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Пользователь неактивен или удален."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Произошла ошибка сервера."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Искаженный запрос."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Некорректные учетные данные."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Учетные данные не были предоставлены."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "У вас нет прав для выполнения этой операции."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Не найдено."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Метод \"{method}\" не разрешен."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Невозможно удовлетворить \"Accept\" заголовок запроса."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Неподдерживаемый тип данных \"{media_type}\" в запросе."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Запрос был проигнорирован."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Это поле обязательно."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Это поле не может быть null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" не является корректным булевым значением."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Это поле не может быть пустым."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Убедитесь что в этом поле не больше {max_length} символов."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Убедитесь что в этом поле как минимум {min_length} символов."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Введите корректный адрес электронной почты."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Значение не соответствует требуемому паттерну."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Введите корректный \"slug\", состоящий из букв, цифр, знаков подчеркивания или дефисов."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Введите корректный URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Требуется целочисленное значение."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Убедитесь что значение меньше или равно {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Убедитесь что значение больше или равно {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Слишком длинное значение."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Требуется численное значение."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Убедитесь что в числе не больше {max_digits} знаков."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Убедитесь что в числе не больше {max_decimal_places} знаков в дробной части."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Убедитесь что в цисле не больше {max_whole_digits} знаков в целой части."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат datetime. Используйте один из этих форматов: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Ожидался datetime, но был получен date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат date. Используйте один из этих форматов: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Ожидался date, но был получен datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат времени. Используйте один из этих форматов: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" не является корректным значением."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Ожидался list со значениями, но был получен \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Не был загружен файл."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Загруженный файл не является корректным файлом. "
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Невозможно определить имя файла."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Загруженный файл пуст."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Убедитесь что имя файла меньше {max_length} символов (сейчас {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Загрузите корректное изображение. Загруженный файл не является изображением, либо является испорченным."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Недопустимая страница \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Недопустимый первичный ключ \"{pk_value}\" - объект не существует."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Некорректный тип. Ожилалось значение первичного ключа, получен {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Недопустимая ссылка - нет совпадения по URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Недопустимая ссылка - некорректное совпадение по URL,"
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Недопустимая ссылка - объект не существует."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Некорректный тип. Ожидался URL, получен {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Объект с {slug_name}={value} не существует."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Недопустимое значение."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Недопустимые данные. Ожидался dictionary, но был получен {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Учетная запись пользователя отключена."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Невозможно войти с предоставленными учетными данными."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Должен включать \"username\" и \"password\"."
diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.mo b/rest_framework/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..53bb95d84
Binary files /dev/null and b/rest_framework/locale/sk/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.po b/rest_framework/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 000000000..9dd378c05
--- /dev/null
+++ b/rest_framework/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Stanislav Komanec , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/django-rest-framework/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Nesprávna hlavička. Neboli poskytnuté prihlasovacie údaje."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Nesprávna hlavička. Prihlasovacie údaje nesmú obsahovať medzery."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Nesprávna hlavička. Prihlasovacie údaje nie sú správne zakódované pomocou metódy base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nesprávne prihlasovacie údaje."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Nesprávna token hlavička. Neboli poskytnuté prihlasovacie údaje."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Nesprávna token hlavička. Token hlavička nesmie obsahovať medzery."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Nesprávny token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Daný používateľ je neaktívny, alebo zmazaný."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Vyskytla sa chyba na strane servera."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Požiadavok má nesprávny formát, alebo je poškodený."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Nesprávne prihlasovacie údaje."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Prihlasovacie údaje neboli zadané."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "K danej akcii nemáte oprávnenie."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nebolo nájdené."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metóda \"{method}\" nie je povolená."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nie je možné vyhovieť požiadavku v hlavičke \"Accept\"."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Požiadavok obsahuje nepodporovaný media type: \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Daný používateľ je zablokovaný."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "S danými prihlasovacími údajmi nebolo možné sa prihlásiť."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musí obsahovať parametre \"používateľské meno\" a \"heslo\"."
diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.mo b/rest_framework/locale/sv/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..a33b0cc58
Binary files /dev/null and b/rest_framework/locale/sv/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.po b/rest_framework/locale/sv/LC_MESSAGES/django.po
new file mode 100644
index 000000000..1602bf55f
--- /dev/null
+++ b/rest_framework/locale/sv/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Joakim Soderlund, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/django-rest-framework/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ogiltig \"basic\"-header. Inga användaruppgifter tillhandahölls."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ogiltig \"basic\"-header. Strängen för användaruppgifterna ska inte innehålla mellanslag."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ogiltig \"basic\"-header. Användaruppgifterna är inte korrekt base64-kodade."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ogiltigt användarnamn/lösenord."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ogiltig \"token\"-header. Inga användaruppgifter tillhandahölls."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ogiltig \"token\"-header. Strängen för referensen ska inte innehålla mellanslag."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ogiltig \"token\"."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Användaren borttagen eller inaktiv."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ett serverfel inträffade."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Ogiltig förfrågan."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Ogiltiga inloggningsuppgifter. "
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Autentiseringsuppgifter ej tillhandahållna."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Du har inte tillåtelse att utföra denna förfrågan."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Hittades inte."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoden \"{method}\" tillåts inte."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kunde inte tillfredsställa förfrågans \"Accept\"-header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Medietypen \"{media_type}\" stöds inte."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Förfrågan stoppades eftersom du har skickat för många."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Det här fältet är obligatoriskt."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Det här fältet får inte vara null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" är inte ett giltigt booleskt värde."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Det här fältet får inte vara blankt."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Se till att detta fält inte har fler än {max_length} tecken."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Se till att detta fält har minst {min_length} tecken."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Ange en giltig mejladress."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Det här värdet matchar inte mallen."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Ange en giltig \"slug\" bestående av bokstäver, nummer, understreck eller bindestreck."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Ange en giltig URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Ett giltigt heltal krävs."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Se till att detta värde är mindre än eller lika med {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Se till att detta värde är större än eller lika med {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Textvärdet är för långt."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Ett giltigt nummer krävs."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Se till att det inte finns fler än totalt {max_digits} siffror."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Se till att det inte finns fler än {max_decimal_places} decimaler."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Se till att det inte finns fler än {max_whole_digits} siffror före decimalpunkten."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datumtiden har fel format. Använd ett av dessa format istället: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Förväntade en datumtid men fick ett datum."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Datumet har fel format. Använde ett av dessa format istället: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Förväntade ett datum men fick en datumtid."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Tiden har fel format. Använd ett av dessa format istället: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" är inte ett giltigt val."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Förväntade en lista med element men fick typen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ingen fil skickades."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Den skickade informationen var inte en fil. Kontrollera formulärets kodningstyp."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Inget filnamn kunde bestämmas."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Den skickade filen var tom."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Se till att det här filnamnet har högst {max_length} tecken (det har {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild eller en skadad bild."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Ogiltigt sida \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Ogiltigt pk \"{pk_value}\" - Objektet finns inte."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Felaktig typ. Förväntade pk-värde, fick {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Ogiltig hyperlänk - Ingen URL matchade."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Ogiltig hyperlänk - Felaktig URL-matching."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Ogiltig hyperlänk - Objektet finns inte."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Felaktig typ. Förväntade URL-sträng, fick {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objekt med {slug_name}={value} finns inte."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ogiltigt värde."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ogiltig data. Förväntade en dictionary, men fick {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Det här fältet måste vara unikt."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Fälten {field_names} måste skapa ett unikt set."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Det här fältet måste vara unikt för datumet \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Det här fältet måste vara unikt för månaden \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Det här fältet måste vara unikt för året \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Ogiltig version i \"Accept\"-headern."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Ogiltig version i URL-resursen."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Ogiltig version i värdnamnet."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Ogiltig version i förfrågningsparametern."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Användarkontot är borttaget."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kunde inte logga in med de angivna inloggningsuppgifterna."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Användarnamn och lösenord måste anges."
diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.mo b/rest_framework/locale/tr/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..e6b848cf5
Binary files /dev/null and b/rest_framework/locale/tr/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.po b/rest_framework/locale/tr/LC_MESSAGES/django.po
new file mode 100644
index 000000000..5aabbebad
--- /dev/null
+++ b/rest_framework/locale/tr/LC_MESSAGES/django.po
@@ -0,0 +1,328 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Ertaç Paprat , 2015
+# Mesut Can Gürle , 2015
+# Recep KIRMIZI , 2015
+# Ülgen Sarıkavak , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Turkish (http://www.transifex.com/projects/p/django-rest-framework/language/tr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Geçersiz kullanıcı adı/parola"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Geçersiz token başlığı. Kimlik bilgileri eksik."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Geçersiz token başlığı. Token'da boşluk olmamalı."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Geçersiz token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Kullanıcı aktif değil ya da silinmiş."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Sunucu hatası oluştu."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Bozuk istek."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Giriş bilgileri hatalı."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Giriş bilgileri verilmedi."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Bu işlemi yapmak için izniniz bulunmuyor."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Bulunamadı."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "\"{method}\" metoduna izin verilmiyor."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "İstekte desteklenmeyen medya tipi: \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Bu alan zorunlu."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Bu alan boş bırakılmamalı."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" geçerli bir boolean değil."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Bu alan boş bırakılmamalı."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Bu alanın {max_length} karakterden fazla karakter barındırmadığından emin olun."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Bu alanın en az {min_length} karakter barındırdığından emin olun."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Geçerli bir e-posta adresi girin."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Bu değer gereken düzenli ifade deseni ile uyuşmuyor."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Harf, rakam, altçizgi veya tireden oluşan geçerli bir \"slug\" giriniz."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Geçerli bir URL girin."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Geçerli bir tam sayı girin."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Değerin {max_value} değerinden küçük ya da eşit olduğundan emin olun."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Değerin {min_value} değerinden büyük ya da eşit olduğundan emin olun."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "String değeri çok uzun."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Geçerli bir numara gerekiyor."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Toplamda {max_digits} haneden fazla hane olmadığından emin olun."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Ondalık basamak değerinin {max_decimal_places} haneden fazla olmadığından emin olun."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Ondalık ayracından önce {max_whole_digits} basamaktan fazla olmadığından emin olun."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime alanı yanlış biçimde. {format} biçimlerinden birini kullanın."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Datetime değeri bekleniyor, ama date değeri geldi."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Tarih biçimi yanlış. {format} biçimlerinden birini kullanın."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Date tipi beklenmekteydi, fakat datetime tipi geldi."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time biçimi yanlış. {format} biçimlerinden birini kullanın."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" geçerli bir seçim değil."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Elemanların listesi beklenirken \"{input_type}\" alındı."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Hiçbir dosya verilmedi."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Gönderilen veri dosya değil. Formdaki kodlama tipini kontrol edin."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Hiçbir dosya adı belirlenemedi."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Gönderilen dosya boş."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Bu dosya adının en fazla {max_length} karakter uzunluğunda olduğundan emin olun. (şu anda {length} karakter)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Geçerli bir resim yükleyin. Yüklediğiniz dosya resim değil ya da bozuk."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Geçersiz sayfa \"{page_number}\":{message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Geçersiz pk \"{pk_value}\" - obje bulunamadı."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Hatalı tip. Pk değeri beklenirken, alınan {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Geçersiz bağlantı - Hiçbir URL eşleşmedi."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Geçersiz bağlantı - Yanlış URL eşleşmesi."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Geçersiz bağlantı - Obje bulunamadı."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Hatalı tip. URL metni bekleniyor, {data_type} alındı."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "{slug_name}={value} değerini taşıyan obje bulunamadı."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Geçersiz değer."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Geçersiz veri. Sözlük bekleniyordu fakat {datatype} geldi. "
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Bu alan eşsiz olmalı."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "{field_names} hep birlikte eşsiz bir küme oluşturmalılar."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Bu alan \"{date_field}\" tarihine göre eşsiz olmalı."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Bu alan \"{date_field}\" ayına göre eşsiz olmalı."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Bu alan \"{date_field}\" yılına göre eşsiz olmalı."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "\"Accept\" başlığındaki sürüm geçersiz."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "URL dizininde geçersiz versiyon."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Host adında geçersiz versiyon."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Sorgu parametresinde geçersiz versiyon."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Kullanıcı hesabı devre dışı bırakılmış."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Verilen bilgiler ile giriş sağlanamadı."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"Kullanıcı Adı\" ve \"Parola\" eklenmeli."
diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.mo b/rest_framework/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..fc3350548
Binary files /dev/null and b/rest_framework/locale/uk/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.po b/rest_framework/locale/uk/LC_MESSAGES/django.po
new file mode 100644
index 000000000..93fc2bf93
--- /dev/null
+++ b/rest_framework/locale/uk/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/django-rest-framework/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo
new file mode 100644
index 000000000..6e7073bd7
Binary files /dev/null and b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo differ
diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644
index 000000000..011288591
--- /dev/null
+++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Lele Long , 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie \n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-rest-framework/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "没有提供认证信息(基本认证HTTP头无效)。"
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "认证字符串不应该包含空格(基本认证HTTP头无效)。"
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "认证字符串base64编码错误(基本认证HTTP头无效)。"
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "用户名或者密码错误。"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "没有提供认证信息(认证令牌HTTP头无效)。"
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "认证令牌字符串不应该包含空格(无效的认证令牌HTTP头)。"
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "认证令牌无效。"
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "用户未激活或者已删除。"
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "服务器出现了错误。"
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "畸形的请求。"
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "不正确的身份认证凭据。"
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "身份认证凭据未提供。"
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "您没有执行该操作的权限。"
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "未找到。"
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "方法 “{method}” 不被允许。"
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "无法满足Accept HTTP头的请求。"
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "不支持请求中的媒体类型 “{media_type}”。"
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "请求被限速。"
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "这个字段是必填项。"
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "这个值不能为 null。"
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "“{input}” 不是合法的布尔值。"
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "此字段不能为空。"
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "请确保这个字段不能超过 {max_length} 个字符。"
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "请确保这个字段至少包含 {min_length} 个字符。"
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "请输入合法的邮件地址。"
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "输入值不匹配要求的模式。"
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "请输入合法的“短语“,只能包含字母,数字,下划线或者中划线。"
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "请输入合法的URL。"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "请填写合法的整数值。"
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "请确保该值小于或者等于 {max_value}。"
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "请确保该值大于或者等于 {min_value}。"
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "字符值太长。"
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "请填写合法的数字。"
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "请确保总计不超过 {max_digits} 个数字。"
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "请确保总计不超过 {max_decimal_places} 个小数位。"
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "请确保小数点前不超过 {max_whole_digits} 个数字。"
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "日期时间格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "期望为日期时间,得到的是日期。"
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "日期格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "期望为日期,得到的是日期时间。"
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "时间格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "“{input}” 不是合法选项。"
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "期望为一个包含物件的列表,得到的类型是“{input_type}”。"
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "没有提交任何文件。"
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "提交的数据不是一个文件。请检查表单的编码类型。"
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "无法检测到文件名。"
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "提交的是空文件。"
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "确保该文件名最多包含 {max_length} 个字符 ( 当前长度为{length} ) 。"
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "请上传有效图片。您上传的该文件不是图片或者图片已经损坏。"
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "无效页面 “{page_number}”:{message}。"
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "无效主键 “{pk_value}” - 对象不存在。"
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "类型错误。期望为主键,得到的类型为 {data_type}。"
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "无效超链接 -没有匹配的URL。"
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "无效超链接 -错误的URL匹配。"
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "无效超链接 -对象不存在。"
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "类型错误。期望为URL字符串,得到的类型是 {data_type}。"
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "属性 {slug_name} 为 {value} 的对象不存在。"
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "无效值。"
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "无效数据。期待为字典类型,得到的是 {datatype} 。"
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "该字段必须唯一。"
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "字段 {field_names} 必须能构成唯一集合。"
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "该字段必须在日期 “{date_field}” 唯一。"
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "该字段必须在月份 “{date_field}” 唯一。"
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "该字段必须在年 “{date_field}” 唯一。"
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "“Accept” HTTP头包含无效版本。"
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "URl路径包含无效版本。"
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "主机名包含无效版本。"
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "请求参数里包含无效版本。"
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "用户账户已禁用。"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "无法使用提供的认证信息登录。"
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "必须包含 “用户名” 和 “密码”。"
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
new file mode 100644
index 000000000..bf3611aa3
--- /dev/null
+++ b/rest_framework/metadata.py
@@ -0,0 +1,138 @@
+"""
+The metadata API is used to allow customization of how `OPTIONS` requests
+are handled. We currently provide a single default implementation that returns
+some fairly ad-hoc information about the view.
+
+Future implementations might use JSON schema or other definitions in order
+to return this information in a more standardized way.
+"""
+from __future__ import unicode_literals
+
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.utils.encoding import force_text
+from rest_framework import exceptions, serializers
+from rest_framework.compat import OrderedDict
+from rest_framework.request import clone_request
+from rest_framework.utils.field_mapping import ClassLookupDict
+
+
+class BaseMetadata(object):
+ def determine_metadata(self, request, view):
+ """
+ Return a dictionary of metadata about the view.
+ Used to return responses for OPTIONS requests.
+ """
+ raise NotImplementedError(".determine_metadata() must be overridden.")
+
+
+class SimpleMetadata(BaseMetadata):
+ """
+ This is the default metadata implementation.
+ It returns an ad-hoc set of information about the view.
+ There are not any formalized standards for `OPTIONS` responses
+ for us to base this on.
+ """
+ label_lookup = ClassLookupDict({
+ serializers.Field: 'field',
+ serializers.BooleanField: 'boolean',
+ serializers.CharField: 'string',
+ serializers.URLField: 'url',
+ serializers.EmailField: 'email',
+ serializers.RegexField: 'regex',
+ serializers.SlugField: 'slug',
+ serializers.IntegerField: 'integer',
+ serializers.FloatField: 'float',
+ serializers.DecimalField: 'decimal',
+ serializers.DateField: 'date',
+ serializers.DateTimeField: 'datetime',
+ serializers.TimeField: 'time',
+ serializers.ChoiceField: 'choice',
+ serializers.MultipleChoiceField: 'multiple choice',
+ serializers.FileField: 'file upload',
+ serializers.ImageField: 'image upload',
+ })
+
+ def determine_metadata(self, request, view):
+ metadata = OrderedDict()
+ metadata['name'] = view.get_view_name()
+ metadata['description'] = view.get_view_description()
+ metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
+ metadata['parses'] = [parser.media_type for parser in view.parser_classes]
+ if hasattr(view, 'get_serializer'):
+ actions = self.determine_actions(request, view)
+ if actions:
+ metadata['actions'] = actions
+ return metadata
+
+ def determine_actions(self, request, view):
+ """
+ For generic class based views we return information about
+ the fields that are accepted for 'PUT' and 'POST' methods.
+ """
+ actions = {}
+ for method in set(['PUT', 'POST']) & set(view.allowed_methods):
+ view.request = clone_request(request, method)
+ try:
+ # Test global permissions
+ if hasattr(view, 'check_permissions'):
+ view.check_permissions(view.request)
+ # Test object permissions
+ if method == 'PUT' and hasattr(view, 'get_object'):
+ view.get_object()
+ except (exceptions.APIException, PermissionDenied, Http404):
+ pass
+ else:
+ # If user has appropriate permissions for the view, include
+ # appropriate metadata about the fields that should be supplied.
+ serializer = view.get_serializer()
+ actions[method] = self.get_serializer_info(serializer)
+ finally:
+ view.request = request
+
+ return actions
+
+ def get_serializer_info(self, serializer):
+ """
+ Given an instance of a serializer, return a dictionary of metadata
+ about its fields.
+ """
+ if hasattr(serializer, 'child'):
+ # If this is a `ListSerializer` then we want to examine the
+ # underlying child serializer instance instead.
+ serializer = serializer.child
+ return OrderedDict([
+ (field_name, self.get_field_info(field))
+ for field_name, field in serializer.fields.items()
+ ])
+
+ def get_field_info(self, field):
+ """
+ Given an instance of a serializer field, return a dictionary
+ of metadata about it.
+ """
+ field_info = OrderedDict()
+ field_info['type'] = self.label_lookup[field]
+ field_info['required'] = getattr(field, 'required', False)
+
+ attrs = [
+ 'read_only', 'label', 'help_text',
+ 'min_length', 'max_length',
+ 'min_value', 'max_value'
+ ]
+
+ for attr in attrs:
+ value = getattr(field, attr, None)
+ if value is not None and value != '':
+ field_info[attr] = force_text(value, strings_only=True)
+
+ if hasattr(field, 'choices'):
+ field_info['choices'] = [
+ {
+ 'value': choice_value,
+ 'display_name': force_text(choice_name, strings_only=True)
+ }
+ for choice_value, choice_name in field.choices.items()
+ ]
+
+ return field_info
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index f11def6d4..c34cfcee1 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -5,39 +5,9 @@ We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
-
-from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
-from rest_framework.request import clone_request
-import warnings
-
-
-def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
- """
- Given a model instance, and an optional pk and slug field,
- return the full list of all other field names on that model.
-
- For use when performing full_clean on a model instance,
- so we only clean the required fields.
- """
- include = []
-
- if pk:
- # Pending deprecation
- pk_field = obj._meta.pk
- while pk_field.rel:
- pk_field = pk_field.rel.to._meta.pk
- include.append(pk_field.name)
-
- if slug_field:
- # Pending deprecation
- include.append(slug_field)
-
- if lookup_field and lookup_field != 'pk':
- include.append(lookup_field)
-
- return [field.name for field in obj._meta.fields if field.name not in include]
+from rest_framework.settings import api_settings
class CreateModelMixin(object):
@@ -45,21 +15,18 @@ class CreateModelMixin(object):
Create a model instance.
"""
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ headers = self.get_success_headers(serializer.data)
+ return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
- if serializer.is_valid():
- self.pre_save(serializer.object)
- self.object = serializer.save(force_insert=True)
- self.post_save(self.object, created=True)
- headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED,
- headers=headers)
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ def perform_create(self, serializer):
+ serializer.save()
def get_success_headers(self, data):
try:
- return {'Location': data['url']}
+ return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
@@ -68,31 +35,15 @@ class ListModelMixin(object):
"""
List a queryset.
"""
- empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
-
def list(self, request, *args, **kwargs):
- self.object_list = self.filter_queryset(self.get_queryset())
+ queryset = self.filter_queryset(self.get_queryset())
- # Default is to allow empty querysets. This can be altered by setting
- # `.allow_empty = False`, to raise 404 errors on empty querysets.
- if not self.allow_empty and not self.object_list:
- warnings.warn(
- 'The `allow_empty` parameter is due to be deprecated. '
- 'To use `allow_empty=False` style behavior, You should override '
- '`get_queryset()` and explicitly raise a 404 on empty querysets.',
- PendingDeprecationWarning
- )
- class_name = self.__class__.__name__
- error_msg = self.empty_error % {'class_name': class_name}
- raise Http404(error_msg)
-
- # Switch between paginated or standard style responses
- page = self.paginate_queryset(self.object_list)
+ page = self.paginate_queryset(queryset)
if page is not None:
- serializer = self.get_pagination_serializer(page)
- else:
- serializer = self.get_serializer(self.object_list, many=True)
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+ serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@@ -101,8 +52,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
- self.object = self.get_object()
- serializer = self.get_serializer(self.object)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance)
return Response(serializer.data)
@@ -112,73 +63,28 @@ class UpdateModelMixin(object):
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
- self.object = self.get_object_or_none()
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ self.perform_update(serializer)
+ return Response(serializer.data)
- if self.object is None:
- created = True
- save_kwargs = {'force_insert': True}
- success_status_code = status.HTTP_201_CREATED
- else:
- created = False
- save_kwargs = {'force_update': True}
- success_status_code = status.HTTP_200_OK
-
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
-
- if serializer.is_valid():
- self.pre_save(serializer.object)
- self.object = serializer.save(**save_kwargs)
- self.post_save(self.object, created=created)
- return Response(serializer.data, status=success_status_code)
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ def perform_update(self, serializer):
+ serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
- def get_object_or_none(self):
- try:
- return self.get_object()
- except Http404:
- # If this is a PUT-as-create operation, we need to ensure that
- # we have relevant permissions, as if this was a POST request.
- # This will either raise a PermissionDenied exception,
- # or simply return None
- self.check_permissions(clone_request(self.request, 'POST'))
-
- def pre_save(self, obj):
- """
- Set any attributes on the object that are implicit in the request.
- """
- # pk and/or slug attributes are implicit in the URL.
- lookup = self.kwargs.get(self.lookup_field, None)
- pk = self.kwargs.get(self.pk_url_kwarg, None)
- slug = self.kwargs.get(self.slug_url_kwarg, None)
- slug_field = slug and self.slug_field or None
-
- if lookup:
- setattr(obj, self.lookup_field, lookup)
-
- if pk:
- setattr(obj, 'pk', pk)
-
- if slug:
- setattr(obj, slug_field, slug)
-
- # Ensure we clean the attributes so that we don't eg return integer
- # pk using a string representation, as provided by the url conf kwarg.
- if hasattr(obj, 'full_clean'):
- exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field)
- obj.full_clean(exclude)
-
class DestroyModelMixin(object):
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
- obj = self.get_object()
- obj.delete()
+ instance = self.get_object()
+ self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
+
+ def perform_destroy(self, instance):
+ instance.delete()
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 4d205c0e8..1838130a9 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
"""
# Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE
- format = format_suffix or request.QUERY_PARAMS.get(format_query_param)
+ format = format_suffix or request.query_params.get(format_query_param)
if format:
renderers = self.filter_renderers(renderers, format)
@@ -54,8 +54,10 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted.
- if (_MediaType(renderer.media_type).precedence >
- _MediaType(media_type).precedence):
+ if (
+ _MediaType(renderer.media_type).precedence >
+ _MediaType(media_type).precedence
+ ):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
return renderer, renderer.media_type
@@ -85,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json"
"""
header = request.META.get('HTTP_ACCEPT', '*/*')
- header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header)
+ header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')]
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d51ea929b..f41a9ae1a 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -1,94 +1,729 @@
+# coding: utf-8
"""
Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
-from rest_framework import serializers
-from rest_framework.templatetags.rest_framework import replace_query_param
+from base64 import b64encode, b64decode
+from collections import namedtuple
+from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.template import Context, loader
+from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.translation import ugettext as _
+from rest_framework.compat import OrderedDict
+from rest_framework.exceptions import NotFound
+from rest_framework.response import Response
+from rest_framework.settings import api_settings
+from rest_framework.utils.urls import (
+ replace_query_param, remove_query_param
+)
+import warnings
-class NextPageField(serializers.Field):
+def _positive_int(integer_string, strict=False, cutoff=None):
"""
- Field that returns a link to the next page in paginated results.
+ Cast a string to a strictly positive integer.
"""
- page_field = 'page'
-
- def to_native(self, value):
- if not value.has_next():
- return None
- page = value.next_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ ret = int(integer_string)
+ if ret < 0 or (ret == 0 and strict):
+ raise ValueError()
+ if cutoff:
+ ret = min(ret, cutoff)
+ return ret
-class PreviousPageField(serializers.Field):
+def _divide_with_ceil(a, b):
"""
- Field that returns a link to the previous page in paginated results.
+ Returns 'a' divded by 'b', with any remainder rounded up.
"""
- page_field = 'page'
-
- def to_native(self, value):
- if not value.has_previous():
- return None
- page = value.previous_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+ if a % b:
+ return (a // b) + 1
+ return a // b
-class DefaultObjectSerializer(serializers.Field):
+def _get_count(queryset):
"""
- If no object serializer is specified, then this serializer will be applied
- as the default.
+ Determine an object count, supporting either querysets or regular lists.
"""
-
- def __init__(self, source=None, context=None):
- # Note: Swallow context kwarg - only required for eg. ModelSerializer.
- super(DefaultObjectSerializer, self).__init__(source=source)
+ try:
+ return queryset.count()
+ except (AttributeError, TypeError):
+ return len(queryset)
-class PaginationSerializerOptions(serializers.SerializerOptions):
+def _get_displayed_page_numbers(current, final):
"""
- An object that stores the options that may be provided to a
- pagination serializer by using the inner `Meta` class.
+ This utility function determines a list of page numbers to display.
+ This gives us a nice contextually relevant set of page numbers.
- Accessible on the instance as `serializer.opts`.
+ For example:
+ current=14, final=16 -> [1, None, 13, 14, 15, 16]
+
+ This implementation gives one page to each side of the cursor,
+ or two pages to the side when the cursor is at the edge, then
+ ensures that any breaks between non-continous page numbers never
+ remove only a single page.
+
+ For an alernativative implementation which gives two pages to each side of
+ the cursor, eg. as in GitHub issue list pagination, see:
+
+ https://gist.github.com/tomchristie/321140cebb1c4a558b15
"""
- def __init__(self, meta):
- super(PaginationSerializerOptions, self).__init__(meta)
- self.object_serializer_class = getattr(meta, 'object_serializer_class',
- DefaultObjectSerializer)
+ assert current >= 1
+ assert final >= current
+
+ if final <= 5:
+ return list(range(1, final + 1))
+
+ # We always include the first two pages, last two pages, and
+ # two pages either side of the current page.
+ included = set((
+ 1,
+ current - 1, current, current + 1,
+ final
+ ))
+
+ # If the break would only exclude a single page number then we
+ # may as well include the page number instead of the break.
+ if current <= 4:
+ included.add(2)
+ included.add(3)
+ if current >= final - 3:
+ included.add(final - 1)
+ included.add(final - 2)
+
+ # Now sort the page numbers and drop anything outside the limits.
+ included = [
+ idx for idx in sorted(list(included))
+ if idx > 0 and idx <= final
+ ]
+
+ # Finally insert any `...` breaks
+ if current > 4:
+ included.insert(1, None)
+ if current < final - 3:
+ included.insert(len(included) - 1, None)
+ return included
-class BasePaginationSerializer(serializers.Serializer):
+def _get_page_links(page_numbers, current, url_func):
"""
- A base class for pagination serializers to inherit from,
- to make implementing custom serializers more easy.
+ Given a list of page numbers and `None` page breaks,
+ return a list of `PageLink` objects.
"""
- _options_class = PaginationSerializerOptions
- results_field = 'results'
-
- def __init__(self, *args, **kwargs):
- """
- Override init to add in the object serializer field on-the-fly.
- """
- super(BasePaginationSerializer, self).__init__(*args, **kwargs)
- results_field = self.results_field
- object_serializer = self.opts.object_serializer_class
-
- if 'context' in kwargs:
- context_kwarg = {'context': kwargs['context']}
+ page_links = []
+ for page_number in page_numbers:
+ if page_number is None:
+ page_link = PAGE_BREAK
else:
- context_kwarg = {}
-
- self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
+ page_link = PageLink(
+ url=url_func(page_number),
+ number=page_number,
+ is_active=(page_number == current),
+ is_break=False
+ )
+ page_links.append(page_link)
+ return page_links
-class PaginationSerializer(BasePaginationSerializer):
+def _decode_cursor(encoded):
"""
- A default implementation of a pagination serializer.
+ Given a string representing an encoded cursor, return a `Cursor` instance.
"""
- count = serializers.Field(source='paginator.count')
- next = NextPageField(source='*')
- previous = PreviousPageField(source='*')
+
+ # The offset in the cursor is used in situations where we have a
+ # nearly-unique index. (Eg millisecond precision creation timestamps)
+ # We guard against malicious users attempting to cause expensive database
+ # queries, by having a hard cap on the maximum possible size of the offset.
+ OFFSET_CUTOFF = 1000
+
+ try:
+ querystring = b64decode(encoded.encode('ascii')).decode('ascii')
+ tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
+
+ offset = tokens.get('o', ['0'])[0]
+ offset = _positive_int(offset, cutoff=OFFSET_CUTOFF)
+
+ reverse = tokens.get('r', ['0'])[0]
+ reverse = bool(int(reverse))
+
+ position = tokens.get('p', [None])[0]
+ except (TypeError, ValueError):
+ return None
+
+ return Cursor(offset=offset, reverse=reverse, position=position)
+
+
+def _encode_cursor(cursor):
+ """
+ Given a Cursor instance, return an encoded string representation.
+ """
+ tokens = {}
+ if cursor.offset != 0:
+ tokens['o'] = str(cursor.offset)
+ if cursor.reverse:
+ tokens['r'] = '1'
+ if cursor.position is not None:
+ tokens['p'] = cursor.position
+
+ querystring = urlparse.urlencode(tokens, doseq=True)
+ return b64encode(querystring.encode('ascii')).decode('ascii')
+
+
+def _reverse_ordering(ordering_tuple):
+ """
+ Given an order_by tuple such as `('-created', 'uuid')` reverse the
+ ordering and return a new tuple, eg. `('created', '-uuid')`.
+ """
+ def invert(x):
+ return x[1:] if (x.startswith('-')) else '-' + x
+
+ return tuple([invert(item) for item in ordering_tuple])
+
+
+Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position'])
+PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
+
+PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
+
+
+class BasePagination(object):
+ display_page_controls = False
+
+ def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
+ raise NotImplementedError('paginate_queryset() must be implemented.')
+
+ def get_paginated_response(self, data): # pragma: no cover
+ raise NotImplementedError('get_paginated_response() must be implemented.')
+
+ def to_html(self): # pragma: no cover
+ raise NotImplementedError('to_html() must be implemented to display page controls.')
+
+
+class PageNumberPagination(BasePagination):
+ """
+ A simple page number based style that supports page numbers as
+ query parameters. For example:
+
+ http://api.example.org/accounts/?page=4
+ http://api.example.org/accounts/?page=4&page_size=100
+ """
+ # The default page size.
+ # Defaults to `None`, meaning pagination is disabled.
+ page_size = api_settings.PAGE_SIZE
+
+ # Client can control the page using this query parameter.
+ page_query_param = 'page'
+
+ # Client can control the page size using this query parameter.
+ # Default is 'None'. Set to eg 'page_size' to enable usage.
+ page_size_query_param = None
+
+ # Set to an integer to limit the maximum page size the client may request.
+ # Only relevant if 'page_size_query_param' has also been set.
+ max_page_size = None
+
+ last_page_strings = ('last',)
+
+ template = 'rest_framework/pagination/numbers.html'
+
+ invalid_page_message = _('Invalid page "{page_number}": {message}.')
+
+ def _handle_backwards_compat(self, view):
+ """
+ Prior to version 3.1, pagination was handled in the view, and the
+ attributes were set there. The attributes should now be set on
+ the pagination class, but the old style is still pending deprecation.
+ """
+ assert not (
+ getattr(view, 'pagination_serializer_class', None) or
+ getattr(api_settings, 'DEFAULT_PAGINATION_SERIALIZER_CLASS', None)
+ ), (
+ "The pagination_serializer_class attribute and "
+ "DEFAULT_PAGINATION_SERIALIZER_CLASS setting have been removed as "
+ "part of the 3.1 pagination API improvement. See the pagination "
+ "documentation for details on the new API."
+ )
+
+ for (settings_key, attr_name) in (
+ ('PAGINATE_BY', 'page_size'),
+ ('PAGINATE_BY_PARAM', 'page_size_query_param'),
+ ('MAX_PAGINATE_BY', 'max_page_size')
+ ):
+ value = getattr(api_settings, settings_key, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` settings key is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ settings_key, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
+
+ for (view_attr, attr_name) in (
+ ('paginate_by', 'page_size'),
+ ('page_query_param', 'page_query_param'),
+ ('paginate_by_param', 'page_size_query_param'),
+ ('max_paginate_by', 'max_page_size')
+ ):
+ value = getattr(view, view_attr, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` view attribute is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ view_attr, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
+
+ def paginate_queryset(self, queryset, request, view=None):
+ """
+ Paginate a queryset if required, either returning a
+ page object, or `None` if pagination is not configured for this view.
+ """
+ self._handle_backwards_compat(view)
+
+ page_size = self.get_page_size(request)
+ if not page_size:
+ return None
+
+ paginator = DjangoPaginator(queryset, page_size)
+ page_number = request.query_params.get(self.page_query_param, 1)
+ if page_number in self.last_page_strings:
+ page_number = paginator.num_pages
+
+ try:
+ self.page = paginator.page(page_number)
+ except InvalidPage as exc:
+ msg = self.invalid_page_message.format(
+ page_number=page_number, message=six.text_type(exc)
+ )
+ raise NotFound(msg)
+
+ if paginator.count > 1 and self.template is not None:
+ # The browsable API should display pagination controls.
+ self.display_page_controls = True
+
+ self.request = request
+ return list(self.page)
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_page_size(self, request):
+ if self.page_size_query_param:
+ try:
+ return _positive_int(
+ request.query_params[self.page_size_query_param],
+ strict=True,
+ cutoff=self.max_page_size
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.page_size
+
+ def get_next_link(self):
+ if not self.page.has_next():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.next_page_number()
+ return replace_query_param(url, self.page_query_param, page_number)
+
+ def get_previous_link(self):
+ if not self.page.has_previous():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.previous_page_number()
+ if page_number == 1:
+ return remove_query_param(url, self.page_query_param)
+ return replace_query_param(url, self.page_query_param, page_number)
+
+ def get_html_context(self):
+ base_url = self.request.build_absolute_uri()
+
+ def page_number_to_url(page_number):
+ if page_number == 1:
+ return remove_query_param(base_url, self.page_query_param)
+ else:
+ return replace_query_param(base_url, self.page_query_param, page_number)
+
+ current = self.page.number
+ final = self.page.paginator.num_pages
+ page_numbers = _get_displayed_page_numbers(current, final)
+ page_links = _get_page_links(page_numbers, current, page_number_to_url)
+
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ }
+
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
+
+
+class LimitOffsetPagination(BasePagination):
+ """
+ A limit/offset based style. For example:
+
+ http://api.example.org/accounts/?limit=100
+ http://api.example.org/accounts/?offset=400&limit=100
+ """
+ default_limit = api_settings.PAGE_SIZE
+ limit_query_param = 'limit'
+ offset_query_param = 'offset'
+ max_limit = None
+ template = 'rest_framework/pagination/numbers.html'
+
+ def paginate_queryset(self, queryset, request, view=None):
+ self.limit = self.get_limit(request)
+ self.offset = self.get_offset(request)
+ self.count = _get_count(queryset)
+ self.request = request
+ if self.count > self.limit and self.template is not None:
+ self.display_page_controls = True
+ return list(queryset[self.offset:self.offset + self.limit])
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('count', self.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_limit(self, request):
+ if self.limit_query_param:
+ try:
+ return _positive_int(
+ request.query_params[self.limit_query_param],
+ cutoff=self.max_limit
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.default_limit
+
+ def get_offset(self, request):
+ try:
+ return _positive_int(
+ request.query_params[self.offset_query_param],
+ )
+ except (KeyError, ValueError):
+ return 0
+
+ def get_next_link(self):
+ if self.offset + self.limit >= self.count:
+ return None
+
+ url = self.request.build_absolute_uri()
+ offset = self.offset + self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
+
+ def get_previous_link(self):
+ if self.offset <= 0:
+ return None
+
+ url = self.request.build_absolute_uri()
+
+ if self.offset - self.limit <= 0:
+ return remove_query_param(url, self.offset_query_param)
+
+ offset = self.offset - self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
+
+ def get_html_context(self):
+ base_url = self.request.build_absolute_uri()
+ current = _divide_with_ceil(self.offset, self.limit) + 1
+ # The number of pages is a little bit fiddly.
+ # We need to sum both the number of pages from current offset to end
+ # plus the number of pages up to the current offset.
+ # When offset is not strictly divisible by the limit then we may
+ # end up introducing an extra page as an artifact.
+ final = (
+ _divide_with_ceil(self.count - self.offset, self.limit) +
+ _divide_with_ceil(self.offset, self.limit)
+ )
+
+ def page_number_to_url(page_number):
+ if page_number == 1:
+ return remove_query_param(base_url, self.offset_query_param)
+ else:
+ offset = self.offset + ((page_number - current) * self.limit)
+ return replace_query_param(base_url, self.offset_query_param, offset)
+
+ page_numbers = _get_displayed_page_numbers(current, final)
+ page_links = _get_page_links(page_numbers, current, page_number_to_url)
+
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ }
+
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
+
+
+class CursorPagination(BasePagination):
+ """
+ The cursor pagination implementation is neccessarily complex.
+ For an overview of the position/offset style we use, see this post:
+ http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/
+ """
+ cursor_query_param = 'cursor'
+ page_size = api_settings.PAGE_SIZE
+ invalid_cursor_message = _('Invalid cursor')
+ ordering = '-created'
+ template = 'rest_framework/pagination/previous_and_next.html'
+
+ def paginate_queryset(self, queryset, request, view=None):
+ self.base_url = request.build_absolute_uri()
+ self.ordering = self.get_ordering(request, queryset, view)
+
+ # Determine if we have a cursor, and if so then decode it.
+ encoded = request.query_params.get(self.cursor_query_param)
+ if encoded is None:
+ self.cursor = None
+ (offset, reverse, current_position) = (0, False, None)
+ else:
+ self.cursor = _decode_cursor(encoded)
+ if self.cursor is None:
+ raise NotFound(self.invalid_cursor_message)
+ (offset, reverse, current_position) = self.cursor
+
+ # Cursor pagination always enforces an ordering.
+ if reverse:
+ queryset = queryset.order_by(*_reverse_ordering(self.ordering))
+ else:
+ queryset = queryset.order_by(*self.ordering)
+
+ # If we have a cursor with a fixed position then filter by that.
+ if current_position is not None:
+ order = self.ordering[0]
+ is_reversed = order.startswith('-')
+ order_attr = order.lstrip('-')
+
+ # Test for: (cursor reversed) XOR (queryset reversed)
+ if self.cursor.reverse != is_reversed:
+ kwargs = {order_attr + '__lt': current_position}
+ else:
+ kwargs = {order_attr + '__gt': current_position}
+
+ queryset = queryset.filter(**kwargs)
+
+ # If we have an offset cursor then offset the entire page by that amount.
+ # We also always fetch an extra item in order to determine if there is a
+ # page following on from this one.
+ results = list(queryset[offset:offset + self.page_size + 1])
+ self.page = list(results[:self.page_size])
+
+ # Determine the position of the final item following the page.
+ if len(results) > len(self.page):
+ has_following_postion = True
+ following_position = self._get_position_from_instance(results[-1], self.ordering)
+ else:
+ has_following_postion = False
+ following_position = None
+
+ # If we have a reverse queryset, then the query ordering was in reverse
+ # so we need to reverse the items again before returning them to the user.
+ if reverse:
+ self.page = list(reversed(self.page))
+
+ if reverse:
+ # Determine next and previous positions for reverse cursors.
+ self.has_next = (current_position is not None) or (offset > 0)
+ self.has_previous = has_following_postion
+ if self.has_next:
+ self.next_position = current_position
+ if self.has_previous:
+ self.previous_position = following_position
+ else:
+ # Determine next and previous positions for forward cursors.
+ self.has_next = has_following_postion
+ self.has_previous = (current_position is not None) or (offset > 0)
+ if self.has_next:
+ self.next_position = following_position
+ if self.has_previous:
+ self.previous_position = current_position
+
+ # Display page controls in the browsable API if there is more
+ # than one page.
+ if (self.has_previous or self.has_next) and self.template is not None:
+ self.display_page_controls = True
+
+ return self.page
+
+ def get_next_link(self):
+ if not self.has_next:
+ return None
+
+ if self.cursor and self.cursor.reverse and self.cursor.offset != 0:
+ # If we're reversing direction and we have an offset cursor
+ # then we cannot use the first position we find as a marker.
+ compare = self._get_position_from_instance(self.page[-1], self.ordering)
+ else:
+ compare = self.next_position
+ offset = 0
+
+ for item in reversed(self.page):
+ position = self._get_position_from_instance(item, self.ordering)
+ if position != compare:
+ # The item in this position and the item following it
+ # have different positions. We can use this position as
+ # our marker.
+ break
+
+ # The item in this postion has the same position as the item
+ # following it, we can't use it as a marker position, so increment
+ # the offset and keep seeking to the previous item.
+ compare = position
+ offset += 1
+
+ else:
+ # There were no unique positions in the page.
+ if not self.has_previous:
+ # We are on the first page.
+ # Our cursor will have an offset equal to the page size,
+ # but no position to filter against yet.
+ offset = self.page_size
+ position = None
+ elif self.cursor.reverse:
+ # The change in direction will introduce a paging artifact,
+ # where we end up skipping forward a few extra items.
+ offset = 0
+ position = self.previous_position
+ else:
+ # Use the position from the existing cursor and increment
+ # it's offset by the page size.
+ offset = self.cursor.offset + self.page_size
+ position = self.previous_position
+
+ cursor = Cursor(offset=offset, reverse=False, position=position)
+ encoded = _encode_cursor(cursor)
+ return replace_query_param(self.base_url, self.cursor_query_param, encoded)
+
+ def get_previous_link(self):
+ if not self.has_previous:
+ return None
+
+ if self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
+ # If we're reversing direction and we have an offset cursor
+ # then we cannot use the first position we find as a marker.
+ compare = self._get_position_from_instance(self.page[0], self.ordering)
+ else:
+ compare = self.previous_position
+ offset = 0
+
+ for item in self.page:
+ position = self._get_position_from_instance(item, self.ordering)
+ if position != compare:
+ # The item in this position and the item following it
+ # have different positions. We can use this position as
+ # our marker.
+ break
+
+ # The item in this postion has the same position as the item
+ # following it, we can't use it as a marker position, so increment
+ # the offset and keep seeking to the previous item.
+ compare = position
+ offset += 1
+
+ else:
+ # There were no unique positions in the page.
+ if not self.has_next:
+ # We are on the final page.
+ # Our cursor will have an offset equal to the page size,
+ # but no position to filter against yet.
+ offset = self.page_size
+ position = None
+ elif self.cursor.reverse:
+ # Use the position from the existing cursor and increment
+ # it's offset by the page size.
+ offset = self.cursor.offset + self.page_size
+ position = self.next_position
+ else:
+ # The change in direction will introduce a paging artifact,
+ # where we end up skipping back a few extra items.
+ offset = 0
+ position = self.next_position
+
+ cursor = Cursor(offset=offset, reverse=True, position=position)
+ encoded = _encode_cursor(cursor)
+ return replace_query_param(self.base_url, self.cursor_query_param, encoded)
+
+ def get_ordering(self, request, queryset, view):
+ """
+ Return a tuple of strings, that may be used in an `order_by` method.
+ """
+ ordering_filters = [
+ filter_cls for filter_cls in getattr(view, 'filter_backends', [])
+ if hasattr(filter_cls, 'get_ordering')
+ ]
+
+ if ordering_filters:
+ # If a filter exists on the view that implements `get_ordering`
+ # then we defer to that filter to determine the ordering.
+ filter_cls = ordering_filters[0]
+ filter_instance = filter_cls()
+ ordering = filter_instance.get_ordering(request, queryset, view)
+ assert ordering is not None, (
+ 'Using cursor pagination, but filter class {filter_cls} '
+ 'returned a `None` ordering.'.format(
+ filter_cls=filter_cls.__name__
+ )
+ )
+ else:
+ # The default case is to check for an `ordering` attribute
+ # on this pagination instance.
+ ordering = self.ordering
+ assert ordering is not None, (
+ 'Using cursor pagination, but no ordering attribute was declared '
+ 'on the pagination class.'
+ )
+
+ assert isinstance(ordering, (six.string_types, list, tuple)), (
+ 'Invalid ordering. Expected string or tuple, but got {type}'.format(
+ type=type(ordering).__name__
+ )
+ )
+
+ if isinstance(ordering, six.string_types):
+ return (ordering,)
+ return tuple(ordering)
+
+ def _get_position_from_instance(self, instance, ordering):
+ attr = getattr(instance, ordering[0].lstrip('-'))
+ return six.text_type(attr)
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_html_context(self):
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link()
+ }
+
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 96bfac84a..437d13392 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -5,17 +5,18 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data.
"""
from __future__ import unicode_literals
+
from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
-from rest_framework.compat import yaml, etree
+from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import force_text
from rest_framework.exceptions import ParseError
-from rest_framework.compat import six
+from rest_framework import renderers
import json
-import datetime
-import decimal
class DataAndFiles(object):
@@ -47,6 +48,7 @@ class JSONParser(BaseParser):
"""
media_type = 'application/json'
+ renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None):
"""
@@ -62,29 +64,6 @@ class JSONParser(BaseParser):
raise ParseError('JSON parse error - %s' % six.text_type(exc))
-class YAMLParser(BaseParser):
- """
- Parses YAML-serialized data.
- """
-
- media_type = 'application/yaml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as YAML and returns the resulting data.
- """
- assert yaml, 'YAMLParser requires pyyaml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
-
- try:
- data = stream.read().decode(encoding)
- return yaml.safe_load(data)
- except (ValueError, yaml.parser.ParserError) as exc:
- raise ParseError('YAML parse error - %s' % six.u(exc))
-
-
class FormParser(BaseParser):
"""
Parser for form data.
@@ -121,7 +100,8 @@ class MultiPartParser(BaseParser):
parser_context = parser_context or {}
request = parser_context['request']
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- meta = request.META
+ meta = request.META.copy()
+ meta['CONTENT_TYPE'] = media_type
upload_handlers = request.upload_handlers
try:
@@ -129,79 +109,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse()
return DataAndFiles(data, files)
except MultiPartParserError as exc:
- raise ParseError('Multipart form parse error - %s' % six.u(exc))
-
-
-class XMLParser(BaseParser):
- """
- XML parser.
- """
-
- media_type = 'application/xml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as XML and returns the resulting data.
- """
- assert etree, 'XMLParser requires defusedxml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- parser = etree.DefusedXMLParser(encoding=encoding)
- try:
- tree = etree.parse(stream, parser=parser, forbid_dtd=True)
- except (etree.ParseError, ValueError) as exc:
- raise ParseError('XML parse error - %s' % six.u(exc))
- data = self._xml_convert(tree.getroot())
-
- return data
-
- def _xml_convert(self, element):
- """
- convert the xml `element` into the corresponding python object
- """
-
- children = list(element)
-
- if len(children) == 0:
- return self._type_convert(element.text)
- else:
- # if the fist child tag is list-item means all children are list-item
- if children[0].tag == "list-item":
- data = []
- for child in children:
- data.append(self._xml_convert(child))
- else:
- data = {}
- for child in children:
- data[child.tag] = self._xml_convert(child)
-
- return data
-
- def _type_convert(self, value):
- """
- Converts the value returned by the XMl parse into the equivalent
- Python type
- """
- if value is None:
- return value
-
- try:
- return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
- except ValueError:
- pass
-
- try:
- return int(value)
- except ValueError:
- pass
-
- try:
- return decimal.Decimal(value)
- except decimal.InvalidOperation:
- pass
-
- return value
+ raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
class FileUploadParser(BaseParser):
@@ -244,7 +152,7 @@ class FileUploadParser(BaseParser):
None,
encoding)
if result is not None:
- return DataAndFiles(None, {'file': result[1]})
+ return DataAndFiles({}, {'file': result[1]})
# This is the standard case.
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
@@ -252,25 +160,26 @@ class FileUploadParser(BaseParser):
chunks = ChunkIter(stream, chunk_size)
counters = [0] * len(upload_handlers)
- for handler in upload_handlers:
+ for index, handler in enumerate(upload_handlers):
try:
handler.new_file(None, filename, content_type,
content_length, encoding)
except StopFutureHandlers:
+ upload_handlers = upload_handlers[:index + 1]
break
for chunk in chunks:
- for i, handler in enumerate(upload_handlers):
+ for index, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
- chunk = handler.receive_data_chunk(chunk, counters[i])
- counters[i] += chunk_length
+ chunk = handler.receive_data_chunk(chunk, counters[index])
+ counters[index] += chunk_length
if chunk is None:
break
- for i, handler in enumerate(upload_handlers):
- file_obj = handler.file_complete(counters[i])
+ for index, handler in enumerate(upload_handlers):
+ file_obj = handler.file_complete(counters[index])
if file_obj:
- return DataAndFiles(None, {'file': file_obj})
+ return DataAndFiles({}, {'file': file_obj})
raise ParseError("FileUpload parse error - "
"none of upload handlers can handle the stream")
@@ -286,7 +195,23 @@ class FileUploadParser(BaseParser):
try:
meta = parser_context['request'].META
- disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'])
- return disposition[1]['filename']
- except (AttributeError, KeyError):
+ disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
+ filename_parm = disposition[1]
+ if 'filename*' in filename_parm:
+ return self.get_encoded_filename(filename_parm)
+ return force_text(filename_parm['filename'])
+ except (AttributeError, KeyError, ValueError):
pass
+
+ def get_encoded_filename(self, filename_parm):
+ """
+ Handle encoded filenames per RFC6266. See also:
+ http://tools.ietf.org/html/rfc2231#section-4
+ """
+ encoded_filename = force_text(filename_parm['filename*'])
+ try:
+ charset, lang, filename = encoded_filename.split('\'', 2)
+ filename = urlparse.unquote(filename)
+ except (ValueError, LookupError):
+ filename = force_text(filename_parm['filename'])
+ return filename
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 1036663e0..9069d315a 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -2,13 +2,11 @@
Provides a set of pluggable permission policies.
"""
from __future__ import unicode_literals
-import inspect
-import warnings
+from django.http import Http404
+from rest_framework.compat import get_model_name
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
-from rest_framework.compat import oauth2_provider_scope, oauth2_constants
-
class BasePermission(object):
"""
@@ -25,13 +23,6 @@ class BasePermission(object):
"""
Return `True` if permission is granted, `False` otherwise.
"""
- if len(inspect.getargspec(self.has_permission).args) == 4:
- warnings.warn(
- 'The `obj` argument in `has_permission` is deprecated. '
- 'Use `has_object_permission()` instead for object permissions.',
- DeprecationWarning, stacklevel=2
- )
- return self.has_permission(request, view, obj)
return True
@@ -52,9 +43,7 @@ class IsAuthenticated(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_authenticated():
- return True
- return False
+ return request.user and request.user.is_authenticated()
class IsAdminUser(BasePermission):
@@ -63,9 +52,7 @@ class IsAdminUser(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_staff:
- return True
- return False
+ return request.user and request.user.is_staff
class IsAuthenticatedOrReadOnly(BasePermission):
@@ -74,11 +61,11 @@ class IsAuthenticatedOrReadOnly(BasePermission):
"""
def has_permission(self, request, view):
- if (request.method in SAFE_METHODS or
+ return (
+ request.method in SAFE_METHODS or
request.user and
- request.user.is_authenticated()):
- return True
- return False
+ request.user.is_authenticated()
+ )
class DjangoModelPermissions(BasePermission):
@@ -115,11 +102,14 @@ class DjangoModelPermissions(BasePermission):
"""
kwargs = {
'app_label': model_cls._meta.app_label,
- 'model_name': model_cls._meta.module_name
+ 'model_name': get_model_name(model_cls)
}
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, view):
+ # Note that `.model` attribute on views is deprecated, although we
+ # enforce the deprecation on the view `get_serializer_class()` and
+ # `get_queryset()` methods, rather than here.
model_cls = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
@@ -136,11 +126,11 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls)
- if (request.user and
+ return (
+ request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and
- request.user.has_perms(perms)):
- return True
- return False
+ request.user.has_perms(perms)
+ )
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
@@ -151,24 +141,60 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
authenticated_users_only = False
-class TokenHasReadWriteScope(BasePermission):
+class DjangoObjectPermissions(DjangoModelPermissions):
"""
- The request is authenticated as a user and the token used has the right scope
+ The request is authenticated using Django's object-level permissions.
+ It requires an object-permissions-enabled backend, such as Django Guardian.
+
+ It ensures that the user is authenticated, and has the appropriate
+ `add`/`change`/`delete` permissions on the object using .has_perms.
+
+ This permission can only be applied against view classes that
+ provide a `.model` or `.queryset` attribute.
"""
- def has_permission(self, request, view):
- token = request.auth
- read_only = request.method in SAFE_METHODS
+ perms_map = {
+ 'GET': [],
+ 'OPTIONS': [],
+ 'HEAD': [],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
- if not token:
+ def get_required_object_permissions(self, method, model_cls):
+ kwargs = {
+ 'app_label': model_cls._meta.app_label,
+ 'model_name': get_model_name(model_cls)
+ }
+ return [perm % kwargs for perm in self.perms_map[method]]
+
+ def has_object_permission(self, request, view, obj):
+ model_cls = getattr(view, 'model', None)
+ queryset = getattr(view, 'queryset', None)
+
+ if model_cls is None and queryset is not None:
+ model_cls = queryset.model
+
+ perms = self.get_required_object_permissions(request.method, model_cls)
+ user = request.user
+
+ if not user.has_perms(perms, obj):
+ # If the user does not have permissions we need to determine if
+ # they have read permissions to see 403, or not, and simply see
+ # a 404 response.
+
+ if request.method in ('GET', 'OPTIONS', 'HEAD'):
+ # Read permissions already checked and failed, no need
+ # to make another lookup.
+ raise Http404
+
+ read_perms = self.get_required_object_permissions('GET', model_cls)
+ if not user.has_perms(read_perms, obj):
+ raise Http404
+
+ # Has read permissions.
return False
- if hasattr(token, 'resource'): # OAuth 1
- return read_only or not request.auth.resource.is_readonly
- elif hasattr(token, 'scope'): # OAuth 2
- required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
- return oauth2_provider_scope.check(required, request.auth.scope)
-
- assert False, ('TokenHasReadWriteScope requires either the'
- '`OAuthAuthentication` or `OAuth2Authentication` authentication '
- 'class to be used.')
+ return True
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index edaf76d6e..3a966c5bf 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,358 +1,192 @@
-"""
-Serializer fields that deal with relationships.
-
-These fields allow you to specify the style that should be used to represent
-model relationships, including hyperlinks, primary keys, or slugs.
-"""
+# coding: utf-8
from __future__ import unicode_literals
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
-from django import forms
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.forms import widgets
-from django.forms.models import ModelChoiceIterator
+from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
+from django.core.urlresolvers import get_script_prefix, resolve, NoReverseMatch, Resolver404
+from django.db.models.query import QuerySet
+from django.utils import six
+from django.utils.encoding import smart_text
+from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
-from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
+from rest_framework.compat import OrderedDict
+from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
-from rest_framework.compat import urlparse
-from rest_framework.compat import smart_text
-import warnings
+from rest_framework.utils import html
-##### Relational fields #####
-
-
-# Not actually Writable, but subclasses may need to be.
-class RelatedField(WritableField):
+class PKOnlyObject(object):
"""
- Base class for related model fields.
-
- This represents a relationship using the unicode representation of the target.
+ This is a mock object, used for when we only need the pk of the object
+ instance, but still want to return an object with a .pk attribute,
+ in order to keep the same interface as a regular model instance.
"""
- widget = widgets.Select
- many_widget = widgets.SelectMultiple
- form_field_class = forms.ChoiceField
- many_form_field_class = forms.MultipleChoiceField
+ def __init__(self, pk):
+ self.pk = pk
- cache_choices = False
- empty_label = None
- read_only = True
- many = False
- def __init__(self, *args, **kwargs):
+# We assume that 'validators' are intended for the child serializer,
+# rather than the parent serializer.
+MANY_RELATION_KWARGS = (
+ 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
+ 'label', 'help_text', 'style', 'error_messages'
+)
- # 'null' is to be deprecated in favor of 'required'
- if 'null' in kwargs:
- warnings.warn('The `null` keyword argument is deprecated. '
- 'Use the `required` keyword argument instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['required'] = not kwargs.pop('null')
- queryset = kwargs.pop('queryset', None)
- self.many = kwargs.pop('many', self.many)
- if self.many:
- self.widget = self.many_widget
- self.form_field_class = self.many_form_field_class
+class RelatedField(Field):
+ def __init__(self, **kwargs):
+ self.queryset = kwargs.pop('queryset', None)
+ assert self.queryset is not None or kwargs.get('read_only', None), (
+ 'Relational field must provide a `queryset` argument, '
+ 'or set read_only=`True`.'
+ )
+ assert not (self.queryset is not None and kwargs.get('read_only', None)), (
+ 'Relational fields should not provide a `queryset` argument, '
+ 'when setting read_only=`True`.'
+ )
+ super(RelatedField, self).__init__(**kwargs)
- kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
- super(RelatedField, self).__init__(*args, **kwargs)
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ManyRelatedField` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return cls.many_init(*args, **kwargs)
+ return super(RelatedField, cls).__new__(cls, *args, **kwargs)
- if not self.required:
- self.empty_label = BLANK_CHOICE_DASH[0][1]
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ """
+ This method handles creating a parent `ManyRelatedField` instance
+ when the `many=True` keyword argument is passed.
- self.queryset = queryset
+ Typically you won't need to override this method.
- def initialize(self, parent, field_name):
- super(RelatedField, self).initialize(parent, field_name)
- if self.queryset is None and not self.read_only:
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomManyRelatedField(*args, **kwargs)
+ """
+ list_kwargs = {'child_relation': cls(*args, **kwargs)}
+ for key in kwargs.keys():
+ if key in MANY_RELATION_KWARGS:
+ list_kwargs[key] = kwargs[key]
+ return ManyRelatedField(**list_kwargs)
+
+ def run_validation(self, data=empty):
+ # We force empty strings to None values for relational fields.
+ if data == '':
+ data = None
+ return super(RelatedField, self).run_validation(data)
+
+ def get_queryset(self):
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated whenever used.
+ queryset = queryset.all()
+ return queryset
+
+ def use_pk_only_optimization(self):
+ return False
+
+ def get_attribute(self, instance):
+ if self.use_pk_only_optimization() and self.source_attrs:
+ # Optimized case, return a mock object only containing the pk attribute.
try:
- manager = getattr(self.parent.opts.model, self.source or field_name)
- if hasattr(manager, 'related'): # Forward
- self.queryset = manager.related.model._default_manager.all()
- else: # Reverse
- self.queryset = manager.field.rel.to._default_manager.all()
- except Exception:
- msg = ('Serializer related fields must include a `queryset`' +
- ' argument or set `read_only=True')
- raise Exception(msg)
+ instance = get_attribute(instance, self.source_attrs[:-1])
+ return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
+ except AttributeError:
+ pass
- ### We need this stuff to make form choices work...
+ # Standard case, return the object instance.
+ return get_attribute(instance, self.source_attrs)
- def prepare_value(self, obj):
- return self.to_native(obj)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
- """
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- def _get_queryset(self):
- return self._queryset
-
- def _set_queryset(self, queryset):
- self._queryset = queryset
- self.widget.choices = self.choices
-
- queryset = property(_get_queryset, _set_queryset)
-
- def _get_choices(self):
- # If self._choices is set, then somebody must have manually set
- # the property self.choices. In this case, just return self._choices.
- if hasattr(self, '_choices'):
- return self._choices
-
- # Otherwise, execute the QuerySet in self.queryset to determine the
- # choices dynamically. Return a fresh ModelChoiceIterator that has not been
- # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
- # time _get_choices() is called (and, thus, each time self.choices is
- # accessed) so that we can ensure the QuerySet has not been consumed. This
- # construct might look complicated but it allows for lazy evaluation of
- # the queryset.
- return ModelChoiceIterator(self)
-
- def _set_choices(self, value):
- # Setting choices also sets the choices on the widget.
- # choices can be any iterable, but we call list() on it because
- # it will be consumed more than once.
- self._choices = self.widget.choices = list(value)
-
- choices = property(_get_choices, _set_choices)
-
- ### Regular serializer stuff...
-
- def field_to_native(self, obj, field_name):
- try:
- if self.source == '*':
- return self.to_native(obj)
-
- source = self.source or field_name
- value = obj
-
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
- except ObjectDoesNotExist:
- return None
-
- if value is None:
- return None
-
- if self.many:
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
- else:
- # Also support non-queryset iterables.
- # This allows us to also support plain lists of related items.
- return [self.to_native(item) for item in value]
- return self.to_native(value)
-
- def field_from_native(self, data, files, field_name, into):
- if self.read_only:
- return
-
- try:
- if self.many:
- try:
- # Form data
- value = data.getlist(field_name)
- if value == [''] or value == []:
- raise KeyError
- except AttributeError:
- # Non-form data
- value = data[field_name]
- else:
- value = data[field_name]
- except KeyError:
- if self.partial:
- return
- value = [] if self.many else None
-
- if value in (None, '') and self.required:
- raise ValidationError(self.error_messages['required'])
- elif value in (None, ''):
- into[(self.source or field_name)] = None
- elif self.many:
- into[(self.source or field_name)] = [self.from_native(item) for item in value]
- else:
- into[(self.source or field_name)] = self.from_native(value)
+ @property
+ def choices(self):
+ return OrderedDict([
+ (
+ six.text_type(self.to_representation(item)),
+ six.text_type(item)
+ )
+ for item in self.queryset.all()
+ ])
-### PrimaryKey relationships
+class StringRelatedField(RelatedField):
+ """
+ A read only field that represents its targets using their
+ plain string representation.
+ """
+
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ super(StringRelatedField, self).__init__(**kwargs)
+
+ def to_representation(self, value):
+ return six.text_type(value)
+
class PrimaryKeyRelatedField(RelatedField):
- """
- Represents a relationship as a pk value.
- """
- read_only = False
-
default_error_messages = {
- 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
+ 'required': _('This field is required.'),
+ 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
}
- # TODO: Remove these field hacks...
- def prepare_value(self, obj):
- return self.to_native(obj.pk)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
- """
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj.pk))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- # TODO: Possibly change this to just take `obj`, through prob less performant
- def to_native(self, pk):
- return pk
-
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
+ def use_pk_only_optimization(self):
+ return True
+ def to_internal_value(self, data):
try:
- return self.queryset.get(pk=data)
+ return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
- msg = self.error_messages['does_not_exist'] % smart_text(data)
- raise ValidationError(msg)
+ self.fail('does_not_exist', pk_value=data)
except (TypeError, ValueError):
- received = type(data).__name__
- msg = self.error_messages['incorrect_type'] % received
- raise ValidationError(msg)
+ self.fail('incorrect_type', data_type=type(data).__name__)
- def field_to_native(self, obj, field_name):
- if self.many:
- # To-many relationship
+ def to_representation(self, value):
+ return value.pk
- queryset = None
- if not self.source:
- # Prefer obj.serializable_value for performance reasons
- try:
- queryset = obj.serializable_value(field_name)
- except AttributeError:
- pass
- if queryset is None:
- # RelatedManager (reverse relationship)
- source = self.source or field_name
- queryset = obj
- for component in source.split('.'):
- queryset = get_component(queryset, component)
-
- # Forward relationship
- if is_simple_callable(getattr(queryset, 'all', None)):
- return [self.to_native(item.pk) for item in queryset.all()]
- else:
- # Also support non-queryset iterables.
- # This allows us to also support plain lists of related items.
- return [self.to_native(item.pk) for item in queryset]
-
- # To-one relationship
- try:
- # Prefer obj.serializable_value for performance reasons
- pk = obj.serializable_value(self.source or field_name)
- except AttributeError:
- # RelatedObject (reverse relationship)
- try:
- pk = getattr(obj, self.source or field_name).pk
- except ObjectDoesNotExist:
- return None
-
- # Forward relationship
- return self.to_native(pk)
-
-
-### Slug relationships
-
-
-class SlugRelatedField(RelatedField):
- """
- Represents a relationship using a unique field on the target.
- """
- read_only = False
-
- default_error_messages = {
- 'does_not_exist': _("Object with %s=%s does not exist."),
- 'invalid': _('Invalid value.'),
- }
-
- def __init__(self, *args, **kwargs):
- self.slug_field = kwargs.pop('slug_field', None)
- assert self.slug_field, 'slug_field is required'
- super(SlugRelatedField, self).__init__(*args, **kwargs)
-
- def to_native(self, obj):
- return getattr(obj, self.slug_field)
-
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- return self.queryset.get(**{self.slug_field: data})
- except ObjectDoesNotExist:
- raise ValidationError(self.error_messages['does_not_exist'] %
- (self.slug_field, smart_text(data)))
- except (TypeError, ValueError):
- msg = self.error_messages['invalid']
- raise ValidationError(msg)
-
-
-### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
- """
- Represents a relationship using hyperlinking.
- """
- read_only = False
lookup_field = 'pk'
default_error_messages = {
- 'no_match': _('Invalid hyperlink - No URL match'),
- 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
- 'configuration_error': _('Invalid hyperlink due to configuration error'),
- 'does_not_exist': _("Invalid hyperlink - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
+ 'required': _('This field is required.'),
+ 'no_match': _('Invalid hyperlink - No URL match.'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
+ 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
}
- # These are all pending deprecation
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
-
- def __init__(self, *args, **kwargs):
- try:
- self.view_name = kwargs.pop('view_name')
- except KeyError:
- raise ValueError("Hyperlinked field requires 'view_name' kwarg")
-
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
+ self.view_name = view_name
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
+ self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
self.format = kwargs.pop('format', None)
- # These are pending deprecation
- if 'pk_url_kwarg' in kwargs:
- msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_url_kwarg' in kwargs:
- msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_field' in kwargs:
- msg = 'slug_field is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
+ # We include this simply for dependency injection in tests.
+ # We can't add it as a class attributes or it would expect an
+ # implicit `self` argument to be passed.
+ self.reverse = reverse
- self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
- self.slug_field = kwargs.pop('slug_field', self.slug_field)
- default_slug_kwarg = self.slug_url_kwarg or self.slug_field
- self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+ super(HyperlinkedRelatedField, self).__init__(**kwargs)
- super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
+ def use_pk_only_optimization(self):
+ return self.lookup_field == 'pk'
+
+ def get_object(self, view_name, view_args, view_kwargs):
+ """
+ Return the object corresponding to a matched URL.
+
+ Takes the matched URL conf arguments, and should return an
+ object instance, or raise an `ObjectDoesNotExist` exception.
+ """
+ lookup_value = view_kwargs[self.lookup_url_kwarg]
+ lookup_kwargs = {self.lookup_field: lookup_value}
+ return self.get_queryset().get(**lookup_kwargs)
def get_url(self, obj, view_name, request, format):
"""
@@ -361,180 +195,57 @@ class HyperlinkedRelatedField(RelatedField):
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
- lookup_field = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_field: lookup_field}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
- if self.pk_url_kwarg != 'pk':
- # Only try pk if it has been explicitly set.
- # Otherwise, the default `lookup_field = 'pk'` has us covered.
- pk = obj.pk
- kwargs = {self.pk_url_kwarg: pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+ lookup_value = getattr(obj, self.lookup_field)
+ kwargs = {self.lookup_url_kwarg: lookup_value}
+ return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
- slug = getattr(obj, self.slug_field, None)
- if slug is not None:
- # Only try slug if it corresponds to an attribute on the object.
- kwargs = {self.slug_url_kwarg: slug}
- try:
- ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
- if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
- # If the lookup succeeds using the default slug params,
- # then `slug_field` is being used implicitly, and we
- # we need to warn about the pending deprecation.
- msg = 'Implicit slug field hyperlinked fields are pending deprecation.' \
- 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- return ret
- except NoReverseMatch:
- pass
-
- raise NoReverseMatch()
-
- def get_object(self, queryset, view_name, view_args, view_kwargs):
- """
- Return the object corresponding to a matched URL.
-
- Takes the matched URL conf arguments, and the queryset, and should
- return an object instance, or raise an `ObjectDoesNotExist` exception.
- """
- lookup = view_kwargs.get(self.lookup_field, None)
- pk = view_kwargs.get(self.pk_url_kwarg, None)
- slug = view_kwargs.get(self.slug_url_kwarg, None)
-
- if lookup is not None:
- filter_kwargs = {self.lookup_field: lookup}
- elif pk is not None:
- filter_kwargs = {'pk': pk}
- elif slug is not None:
- filter_kwargs = {self.slug_field: slug}
- else:
- raise ObjectDoesNotExist()
-
- return queryset.get(**filter_kwargs)
-
- def to_native(self, obj):
- view_name = self.view_name
+ def to_internal_value(self, data):
request = self.context.get('request', None)
- format = self.format or self.context.get('format', None)
-
- if request is None:
- msg = (
- "Using `HyperlinkedRelatedField` without including the request "
- "in the serializer context is deprecated. "
- "Add `context={'request': request}` when instantiating "
- "the serializer."
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=4)
-
- # If the object has not yet been saved then we cannot hyperlink to it.
- if getattr(obj, 'pk', None) is None:
- return
-
- # Return the hyperlink, or error if incorrectly configured.
try:
- return self.get_url(obj, view_name, request, format)
- except NoReverseMatch:
- msg = (
- 'Could not resolve URL for hyperlinked relationship using '
- 'view name "%s". You may have failed to include the related '
- 'model in your API, or incorrectly configured the '
- '`lookup_field` attribute on this field.'
- )
- raise Exception(msg % view_name)
-
- def from_native(self, value):
- # Convert URL -> model instance pk
- # TODO: Use values_list
- queryset = self.queryset
- if queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- http_prefix = value.startswith(('http:', 'https:'))
+ http_prefix = data.startswith(('http:', 'https:'))
except AttributeError:
- msg = self.error_messages['incorrect_type']
- raise ValidationError(msg % type(value).__name__)
+ self.fail('incorrect_type', data_type=type(data).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
- value = urlparse.urlparse(value).path
+ data = urlparse.urlparse(data).path
prefix = get_script_prefix()
- if value.startswith(prefix):
- value = '/' + value[len(prefix):]
+ if data.startswith(prefix):
+ data = '/' + data[len(prefix):]
try:
- match = resolve(value)
- except Exception:
- raise ValidationError(self.error_messages['no_match'])
-
- if match.view_name != self.view_name:
- raise ValidationError(self.error_messages['incorrect_match'])
+ match = resolve(data)
+ except Resolver404:
+ self.fail('no_match')
try:
- return self.get_object(queryset, match.view_name,
- match.args, match.kwargs)
+ expected_viewname = request.versioning_scheme.get_versioned_viewname(
+ self.view_name, request
+ )
+ except AttributeError:
+ expected_viewname = self.view_name
+
+ if match.view_name != expected_viewname:
+ self.fail('incorrect_match')
+
+ try:
+ return self.get_object(match.view_name, match.args, match.kwargs)
except (ObjectDoesNotExist, TypeError, ValueError):
- raise ValidationError(self.error_messages['does_not_exist'])
+ self.fail('does_not_exist')
-
-class HyperlinkedIdentityField(Field):
- """
- Represents the instance, or a property on the instance, using hyperlinking.
- """
- lookup_field = 'pk'
- read_only = True
-
- # These are all pending deprecation
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
-
- def __init__(self, *args, **kwargs):
- try:
- self.view_name = kwargs.pop('view_name')
- except KeyError:
- msg = "HyperlinkedIdentityField requires 'view_name' argument"
- raise ValueError(msg)
-
- self.format = kwargs.pop('format', None)
- lookup_field = kwargs.pop('lookup_field', None)
- self.lookup_field = lookup_field or self.lookup_field
-
- # These are pending deprecation
- if 'pk_url_kwarg' in kwargs:
- msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_url_kwarg' in kwargs:
- msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_field' in kwargs:
- msg = 'slug_field is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
-
- self.slug_field = kwargs.pop('slug_field', self.slug_field)
- default_slug_kwarg = self.slug_url_kwarg or self.slug_field
- self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
- self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
-
- super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
-
- def field_to_native(self, obj, field_name):
+ def to_representation(self, value):
request = self.context.get('request', None)
format = self.context.get('format', None)
- view_name = self.view_name
- if request is None:
- warnings.warn("Using `HyperlinkedIdentityField` without including the "
- "request in the serializer context is deprecated. "
- "Add `context={'request': request}` when instantiating the serializer.",
- DeprecationWarning, stacklevel=4)
+ assert request is not None, (
+ "`%s` requires the request in the serializer"
+ " context. Add `context={'request': request}` when instantiating "
+ "the serializer." % self.__class__.__name__
+ )
# By default use whatever format is given for the current context
# unless the target is a different type to the source.
@@ -550,7 +261,7 @@ class HyperlinkedIdentityField(Field):
# Return the hyperlink, or error if incorrectly configured.
try:
- return self.get_url(obj, view_name, request, format)
+ return self.get_url(value, self.view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
@@ -558,76 +269,122 @@ class HyperlinkedIdentityField(Field):
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
- raise Exception(msg % view_name)
+ raise ImproperlyConfigured(msg % self.view_name)
- def get_url(self, obj, view_name, request, format):
- """
- Given an object, return the URL that hyperlinks to the object.
- May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
- attributes are not configured to correctly match the URL conf.
- """
- lookup_field = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_field: lookup_field}
+class HyperlinkedIdentityField(HyperlinkedRelatedField):
+ """
+ A read-only field that represents the identity URL for an object, itself.
+
+ This is in contrast to `HyperlinkedRelatedField` which represents the
+ URL of relationships to other objects.
+ """
+
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
+ kwargs['read_only'] = True
+ kwargs['source'] = '*'
+ super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
+
+ def use_pk_only_optimization(self):
+ # We have the complete object instance already. We don't need
+ # to run the 'only get the pk for this relationship' code.
+ return False
+
+
+class SlugRelatedField(RelatedField):
+ """
+ A read-write field the represents the target of the relationship
+ by a unique 'slug' attribute.
+ """
+
+ default_error_messages = {
+ 'does_not_exist': _('Object with {slug_name}={value} does not exist.'),
+ 'invalid': _('Invalid value.'),
+ }
+
+ def __init__(self, slug_field=None, **kwargs):
+ assert slug_field is not None, 'The `slug_field` argument is required.'
+ self.slug_field = slug_field
+ super(SlugRelatedField, self).__init__(**kwargs)
+
+ def to_internal_value(self, data):
try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+ return self.get_queryset().get(**{self.slug_field: data})
+ except ObjectDoesNotExist:
+ self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data))
+ except (TypeError, ValueError):
+ self.fail('invalid')
- if self.pk_url_kwarg != 'pk':
- # Only try pk lookup if it has been explicitly set.
- # Otherwise, the default `lookup_field = 'pk'` has us covered.
- kwargs = {self.pk_url_kwarg: obj.pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
-
- slug = getattr(obj, self.slug_field, None)
- if slug:
- # Only use slug lookup if a slug field exists on the model
- kwargs = {self.slug_url_kwarg: slug}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
-
- raise NoReverseMatch()
+ def to_representation(self, obj):
+ return getattr(obj, self.slug_field)
-### Old-style many classes for backwards compat
+class ManyRelatedField(Field):
+ """
+ Relationships with `many=True` transparently get coerced into instead being
+ a ManyRelatedField with a child relationship.
-class ManyRelatedField(RelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyRelatedField()` is deprecated. '
- 'Use `RelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
+ The `ManyRelatedField` class is responsible for handling iterating through
+ the values and passing each one to the child relationship.
+
+ This class is treated as private API.
+ You shouldn't generally need to be using this class directly yourself,
+ and should instead simply set 'many=True' on the relationship.
+ """
+ initial = []
+ default_empty_html = []
+
+ def __init__(self, child_relation=None, *args, **kwargs):
+ self.child_relation = child_relation
+ assert child_relation is not None, '`child_relation` is a required argument.'
super(ManyRelatedField, self).__init__(*args, **kwargs)
+ self.child_relation.bind(field_name='', parent=self)
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ # Don't return [] if the update is partial
+ if self.field_name not in dictionary:
+ if getattr(self.root, 'partial', False):
+ return empty
+ return dictionary.getlist(self.field_name)
-class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. '
- 'Use `PrimaryKeyRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
+ return dictionary.get(self.field_name, empty)
+ def to_internal_value(self, data):
+ return [
+ self.child_relation.to_internal_value(item)
+ for item in data
+ ]
-class ManySlugRelatedField(SlugRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManySlugRelatedField()` is deprecated. '
- 'Use `SlugRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManySlugRelatedField, self).__init__(*args, **kwargs)
+ def get_attribute(self, instance):
+ # Can't have any relationships if not created
+ if not instance.pk:
+ return []
+ relationship = get_attribute(instance, self.source_attrs)
+ return relationship.all() if (hasattr(relationship, 'all')) else relationship
-class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. '
- 'Use `HyperlinkedRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)
+ def to_representation(self, iterable):
+ return [
+ self.child_relation.to_representation(value)
+ for value in iterable
+ ]
+
+ @property
+ def choices(self):
+ queryset = self.child_relation.queryset
+ iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
+ items_and_representations = [
+ (item, self.child_relation.to_representation(item))
+ for item in iterable
+ ]
+ return OrderedDict([
+ (
+ six.text_type(item_representation),
+ six.text_type(item) + ' - ' + six.text_type(item_representation)
+ )
+ for item, item_representation in items_and_representations
+ ])
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 3a03ca332..920d2bc47 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -8,24 +8,27 @@ REST framework also provides an HTML renderer the renders the browsable API.
"""
from __future__ import unicode_literals
-import copy
import json
+import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
+from django.core.paginator import Page
from django.http.multipartparser import parse_header
-from django.template import RequestContext, loader, Template
+from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
-from django.utils.xmlutils import SimplerXMLGenerator
-from rest_framework.compat import StringIO
-from rest_framework.compat import six
-from rest_framework.compat import smart_text
-from rest_framework.compat import yaml
+from django.utils import six
+from rest_framework import exceptions, serializers, status, VERSION
+from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, INDENT_SEPARATORS
+from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
-from rest_framework.request import clone_request
+from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.utils.formatting import get_view_name, get_view_description
-from rest_framework import exceptions, parsers, status, VERSION
+from rest_framework.utils.field_mapping import ClassLookupDict
+
+
+def zero_as_none(value):
+ return None if value == 0 else value
class BaseRenderer(object):
@@ -37,172 +40,79 @@ class BaseRenderer(object):
media_type = None
format = None
charset = 'utf-8'
+ render_style = 'text'
def render(self, data, accepted_media_type=None, renderer_context=None):
- raise NotImplemented('Renderer class requires .render() to be implemented')
+ raise NotImplementedError('Renderer class requires .render() to be implemented')
class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to JSON.
- Applies JSON's backslash-u character escaping for non-ascii characters.
"""
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
- ensure_ascii = True
- charset = 'utf-8'
- # Note that JSON encodings must be utf-8, utf-16 or utf-32.
+ ensure_ascii = not api_settings.UNICODE_JSON
+ compact = api_settings.COMPACT_JSON
+
+ # We don't set a charset because JSON is a binary encoding,
+ # that can be encoded as utf-8, utf-16 or utf-32.
# See: http://www.ietf.org/rfc/rfc4627.txt
+ # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
+ charset = None
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Render `data` into JSON.
- """
- if data is None:
- return ''
-
- # If 'indent' is provided in the context, then pretty print the result.
- # E.g. If we're being called by the BrowsableAPIRenderer.
- renderer_context = renderer_context or {}
- indent = renderer_context.get('indent', None)
-
+ def get_indent(self, accepted_media_type, renderer_context):
if accepted_media_type:
# If the media type looks like 'application/json; indent=4',
# then pretty print the result.
+ # Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
- indent = params.get('indent', indent)
try:
- indent = max(min(int(indent), 8), 0)
- except (ValueError, TypeError):
- indent = None
+ return zero_as_none(max(min(int(params['indent']), 8), 0))
+ except (KeyError, ValueError, TypeError):
+ pass
- ret = json.dumps(data, cls=self.encoder_class,
- indent=indent, ensure_ascii=self.ensure_ascii)
+ # If 'indent' is provided in the context, then pretty print the result.
+ # E.g. If we're being called by the BrowsableAPIRenderer.
+ return renderer_context.get('indent', None)
+
+ def render(self, data, accepted_media_type=None, renderer_context=None):
+ """
+ Render `data` into JSON, returning a bytestring.
+ """
+ if data is None:
+ return bytes()
+
+ renderer_context = renderer_context or {}
+ indent = self.get_indent(accepted_media_type, renderer_context)
+
+ if indent is None:
+ separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS
+ else:
+ separators = INDENT_SEPARATORS
+
+ ret = json.dumps(
+ data, cls=self.encoder_class,
+ indent=indent, ensure_ascii=self.ensure_ascii,
+ separators=separators
+ )
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
# but if ensure_ascii=False, the return type is underspecified,
# and may (or may not) be unicode.
# On python 3.x json.dumps() returns unicode strings.
if isinstance(ret, six.text_type):
- return bytes(ret.encode(self.charset))
+ # We always fully escape \u2028 and \u2029 to ensure we output JSON
+ # that is a strict javascript subset. If bytes were returned
+ # by json.dumps() then we don't have these characters in any case.
+ # See: http://timelessrepo.com/json-isnt-a-javascript-subset
+ ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
+ return bytes(ret.encode('utf-8'))
return ret
-class UnicodeJSONRenderer(JSONRenderer):
- ensure_ascii = False
- charset = 'utf-8'
- """
- Renderer which serializes to JSON.
- Does *not* apply JSON's character escaping for non-ascii characters.
- """
-
-
-class JSONPRenderer(JSONRenderer):
- """
- Renderer which serializes to json,
- wrapping the json output in a callback function.
- """
-
- media_type = 'application/javascript'
- format = 'jsonp'
- callback_parameter = 'callback'
- default_callback = 'callback'
-
- def get_callback(self, renderer_context):
- """
- Determine the name of the callback to wrap around the json output.
- """
- request = renderer_context.get('request', None)
- params = request and request.QUERY_PARAMS or {}
- return params.get(self.callback_parameter, self.default_callback)
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders into jsonp, wrapping the json output in a callback function.
-
- Clients may set the callback function name using a query parameter
- on the URL, for example: ?callback=exampleCallbackName
- """
- renderer_context = renderer_context or {}
- callback = self.get_callback(renderer_context)
- json = super(JSONPRenderer, self).render(data, accepted_media_type,
- renderer_context)
- return callback.encode(self.charset) + b'(' + json + b');'
-
-
-class XMLRenderer(BaseRenderer):
- """
- Renderer which serializes to XML.
- """
-
- media_type = 'application/xml'
- format = 'xml'
- charset = 'utf-8'
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders *obj* into serialized XML.
- """
- if data is None:
- return ''
-
- stream = StringIO()
-
- xml = SimplerXMLGenerator(stream, self.charset)
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
-
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in six.iteritems(data):
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- elif data is None:
- # Don't output any value
- pass
-
- else:
- xml.characters(smart_text(data))
-
-
-class YAMLRenderer(BaseRenderer):
- """
- Renderer which serializes to YAML.
- """
-
- media_type = 'application/yaml'
- format = 'yaml'
- encoder = encoders.SafeDumper
- charset = 'utf-8'
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders *obj* into serialized YAML.
- """
- assert yaml, 'YAMLRenderer requires pyyaml to be installed'
-
- if data is None:
- return ''
-
- return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
-
-
class TemplateHTMLRenderer(BaseRenderer):
"""
An HTML renderer for use with templates.
@@ -271,7 +181,11 @@ class TemplateHTMLRenderer(BaseRenderer):
return [self.template_name]
elif hasattr(view, 'get_template_names'):
return view.get_template_names()
- raise ImproperlyConfigured('Returned a template response with no template_name')
+ elif hasattr(view, 'template_name'):
+ return [view.template_name]
+ raise ImproperlyConfigured(
+ 'Returned a template response with no `template_name` attribute set on either the view or response'
+ )
def get_exception_template(self, response):
template_names = [name % {'status_code': response.status_code}
@@ -317,6 +231,132 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
return data
+class HTMLFormRenderer(BaseRenderer):
+ """
+ Renderers serializer data into an HTML form.
+
+ If the serializer was instantiated without an object then this will
+ return an HTML form not bound to any object,
+ otherwise it will return an HTML form with the appropriate initial data
+ populated from the object.
+
+ Note that rendering of field and form errors is not currently supported.
+ """
+ media_type = 'text/html'
+ format = 'form'
+ charset = 'utf-8'
+ template_pack = 'rest_framework/horizontal/'
+ base_template = 'form.html'
+
+ default_style = ClassLookupDict({
+ serializers.Field: {
+ 'base_template': 'input.html',
+ 'input_type': 'text'
+ },
+ serializers.EmailField: {
+ 'base_template': 'input.html',
+ 'input_type': 'email'
+ },
+ serializers.URLField: {
+ 'base_template': 'input.html',
+ 'input_type': 'url'
+ },
+ serializers.IntegerField: {
+ 'base_template': 'input.html',
+ 'input_type': 'number'
+ },
+ serializers.DateTimeField: {
+ 'base_template': 'input.html',
+ 'input_type': 'datetime-local'
+ },
+ serializers.DateField: {
+ 'base_template': 'input.html',
+ 'input_type': 'date'
+ },
+ serializers.TimeField: {
+ 'base_template': 'input.html',
+ 'input_type': 'time'
+ },
+ serializers.FileField: {
+ 'base_template': 'input.html',
+ 'input_type': 'file'
+ },
+ serializers.BooleanField: {
+ 'base_template': 'checkbox.html'
+ },
+ serializers.ChoiceField: {
+ 'base_template': 'select.html', # Also valid: 'radio.html'
+ },
+ serializers.MultipleChoiceField: {
+ 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
+ },
+ serializers.RelatedField: {
+ 'base_template': 'select.html', # Also valid: 'radio.html'
+ },
+ serializers.ManyRelatedField: {
+ 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
+ },
+ serializers.Serializer: {
+ 'base_template': 'fieldset.html'
+ },
+ serializers.ListSerializer: {
+ 'base_template': 'list_fieldset.html'
+ }
+ })
+
+ def render_field(self, field, parent_style):
+ if isinstance(field._field, serializers.HiddenField):
+ return ''
+
+ style = dict(self.default_style[field])
+ style.update(field.style)
+ if 'template_pack' not in style:
+ style['template_pack'] = parent_style.get('template_pack', self.template_pack)
+ style['renderer'] = self
+
+ if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type):
+ field.value = field.value.rstrip('Z')
+
+ if 'template' in style:
+ template_name = style['template']
+ else:
+ template_name = style['template_pack'].strip('/') + '/' + style['base_template']
+
+ template = loader.get_template(template_name)
+ context = Context({'field': field, 'style': style})
+ return template.render(context)
+
+ def render(self, data, accepted_media_type=None, renderer_context=None):
+ """
+ Render serializer data and return an HTML form, as a string.
+ """
+ form = data.serializer
+ meta = getattr(form, 'Meta', None)
+ style = getattr(meta, 'style', {})
+ if 'template_pack' not in style:
+ style['template_pack'] = self.template_pack
+ if 'base_template' not in style:
+ style['base_template'] = self.base_template
+ style['renderer'] = self
+
+ # This API needs to be finessed and finalized for 3.1
+ if 'template' in renderer_context:
+ template_name = renderer_context['template']
+ elif 'template' in style:
+ template_name = style['template']
+ else:
+ template_name = style['template_pack'].strip('/') + '/' + style['base_template']
+
+ renderer_context = renderer_context or {}
+ request = renderer_context['request']
+ template = loader.get_template(template_name)
+ context = RequestContext(request, {
+ 'form': form,
+ 'style': style
+ })
+ return template.render(context)
+
+
class BrowsableAPIRenderer(BaseRenderer):
"""
HTML renderer used to self-document the API.
@@ -325,6 +365,7 @@ class BrowsableAPIRenderer(BaseRenderer):
format = 'api'
template = 'rest_framework/api.html'
charset = 'utf-8'
+ form_renderer_class = HTMLFormRenderer
def get_default_renderer(self, view):
"""
@@ -333,8 +374,13 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, BrowsableAPIRenderer)]
+ non_template_renderers = [renderer for renderer in renderers
+ if not hasattr(renderer, 'get_template_names')]
+
if not renderers:
return None
+ elif non_template_renderers:
+ return non_template_renderers[0]()
return renderers[0]()
def get_content(self, renderer, data,
@@ -349,7 +395,10 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
- if renderer.charset is None:
+ render_style = getattr(renderer, 'render_style', 'text')
+ assert render_style in ['text', 'binary'], 'Expected .render_style ' \
+ '"text" or "binary", but got "%s"' % render_style
+ if render_style == 'binary':
return '[%d bytes of binary content]' % len(content)
return content
@@ -358,7 +407,7 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
Returns True if a form should be shown for this method.
"""
- if not method in view.allowed_methods:
+ if method not in view.allowed_methods:
return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
@@ -372,202 +421,227 @@ class BrowsableAPIRenderer(BaseRenderer):
return False # Doesn't have permissions
return True
- def serializer_to_form_fields(self, serializer):
- fields = {}
- for k, v in serializer.get_fields().items():
- if getattr(v, 'read_only', True):
- continue
+ def get_rendered_html_form(self, data, view, method, request):
+ """
+ Return a string representing a rendered HTML form, possibly bound to
+ either the input or output data.
+ In the absence of the View having an associated form then return None.
+ """
+ # See issue #2089 for refactoring this.
+ serializer = getattr(data, 'serializer', None)
+ if serializer and not getattr(serializer, 'many', False):
+ instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
+ else:
+ instance = None
+
+ # If this is valid serializer data, and the form is for the same
+ # HTTP method as was used in the request then use the existing
+ # serializer instance, rather than dynamically creating a new one.
+ if request.method == method and serializer is not None:
+ try:
+ kwargs = {'data': request.data}
+ except ParseError:
+ kwargs = {}
+ existing_serializer = serializer
+ else:
kwargs = {}
- kwargs['required'] = v.required
+ existing_serializer = None
- #if getattr(v, 'queryset', None):
- # kwargs['queryset'] = v.queryset
+ with override_method(view, request, method) as request:
+ if not self.show_form_for_method(view, method, request, instance):
+ return
- if getattr(v, 'choices', None) is not None:
- kwargs['choices'] = v.choices
+ if method in ('DELETE', 'OPTIONS'):
+ return True # Don't actually need to return a form
- if getattr(v, 'regex', None) is not None:
- kwargs['regex'] = v.regex
+ if (
+ not getattr(view, 'get_serializer', None) or
+ not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
+ ):
+ return
- if getattr(v, 'widget', None):
- widget = copy.deepcopy(v.widget)
- kwargs['widget'] = widget
+ if existing_serializer is not None:
+ serializer = existing_serializer
+ else:
+ if method in ('PUT', 'PATCH'):
+ serializer = view.get_serializer(instance=instance, **kwargs)
+ else:
+ serializer = view.get_serializer(**kwargs)
- if getattr(v, 'default', None) is not None:
- kwargs['initial'] = v.default
+ if hasattr(serializer, 'initial_data'):
+ serializer.is_valid()
- if getattr(v, 'label', None) is not None:
- kwargs['label'] = v.label
+ form_renderer = self.form_renderer_class()
+ return form_renderer.render(
+ serializer.data,
+ self.accepted_media_type,
+ dict(
+ list(self.renderer_context.items()) +
+ [('template', 'rest_framework/api_form.html')]
+ )
+ )
- if getattr(v, 'help_text', None) is not None:
- kwargs['help_text'] = v.help_text
-
- fields[k] = v.form_field_class(**kwargs)
-
- return fields
-
- def _get_form(self, view, method, request):
- # We need to impersonate a request with the correct method,
- # so that eg. any dynamic get_serializer_class methods return the
- # correct form for each method.
- restore = view.request
- request = clone_request(request, method)
- view.request = request
- try:
- return self.get_form(view, method, request)
- finally:
- view.request = restore
-
- def _get_raw_data_form(self, view, method, request, media_types):
- # We need to impersonate a request with the correct method,
- # so that eg. any dynamic get_serializer_class methods return the
- # correct form for each method.
- restore = view.request
- request = clone_request(request, method)
- view.request = request
- try:
- return self.get_raw_data_form(view, method, request, media_types)
- finally:
- view.request = restore
-
- def get_form(self, view, method, request):
- """
- Get a form, possibly bound to either the input or output data.
- In the absence on of the Resource having an associated form then
- provide a form that can be used to submit arbitrary content.
- """
- obj = getattr(view, 'object', None)
- if not self.show_form_for_method(view, method, request, obj):
- return
-
- if method in ('DELETE', 'OPTIONS'):
- return True # Don't actually need to return a form
-
- if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes:
- return
-
- serializer = view.get_serializer(instance=obj)
- fields = self.serializer_to_form_fields(serializer)
-
- # Creating an on the fly form see:
- # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
- OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields)
- data = (obj is not None) and serializer.data or None
- form_instance = OnTheFlyForm(data)
- return form_instance
-
- def get_raw_data_form(self, view, method, request, media_types):
+ def get_raw_data_form(self, data, view, method, request):
"""
Returns a form that allows for arbitrary content types to be tunneled
via standard HTML forms.
(Which are typically application/x-www-form-urlencoded)
"""
+ # See issue #2089 for refactoring this.
+ serializer = getattr(data, 'serializer', None)
+ if serializer and not getattr(serializer, 'many', False):
+ instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
+ else:
+ instance = None
- # If we're not using content overloading there's no point in supplying a generic form,
- # as the view won't treat the form's value as the content of the request.
- if not (api_settings.FORM_CONTENT_OVERRIDE
- and api_settings.FORM_CONTENTTYPE_OVERRIDE):
- return None
+ with override_method(view, request, method) as request:
+ # If we're not using content overloading there's no point in
+ # supplying a generic form, as the view won't treat the form's
+ # value as the content of the request.
+ if not (api_settings.FORM_CONTENT_OVERRIDE and
+ api_settings.FORM_CONTENTTYPE_OVERRIDE):
+ return None
- # Check permissions
- obj = getattr(view, 'object', None)
- if not self.show_form_for_method(view, method, request, obj):
- return
+ # Check permissions
+ if not self.show_form_for_method(view, method, request, instance):
+ return
- content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
- content_field = api_settings.FORM_CONTENT_OVERRIDE
- choices = [(media_type, media_type) for media_type in media_types]
- initial = media_types[0]
+ # If possible, serialize the initial content for the generic form
+ default_parser = view.parser_classes[0]
+ renderer_class = getattr(default_parser, 'renderer_class', None)
+ if (hasattr(view, 'get_serializer') and renderer_class):
+ # View has a serializer defined and parser class has a
+ # corresponding renderer that can be used to render the data.
- # NB. http://jacobian.org/writing/dynamic-form-generation/
- class GenericContentForm(forms.Form):
- def __init__(self):
- super(GenericContentForm, self).__init__()
+ if method in ('PUT', 'PATCH'):
+ serializer = view.get_serializer(instance=instance)
+ else:
+ serializer = view.get_serializer()
- self.fields[content_type_field] = forms.ChoiceField(
- label='Media type',
- choices=choices,
- initial=initial
- )
- self.fields[content_field] = forms.CharField(
- label='Content',
- widget=forms.Textarea
- )
+ # Render the raw data content
+ renderer = renderer_class()
+ accepted = self.accepted_media_type
+ context = self.renderer_context.copy()
+ context['indent'] = 4
+ content = renderer.render(serializer.data, accepted, context)
+ else:
+ content = None
- return GenericContentForm()
+ # Generate a generic form that includes a content type field,
+ # and a content field.
+ content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
+ content_field = api_settings.FORM_CONTENT_OVERRIDE
+
+ media_types = [parser.media_type for parser in view.parser_classes]
+ choices = [(media_type, media_type) for media_type in media_types]
+ initial = media_types[0]
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self):
+ super(GenericContentForm, self).__init__()
+
+ self.fields[content_type_field] = forms.ChoiceField(
+ label='Media type',
+ choices=choices,
+ initial=initial
+ )
+ self.fields[content_field] = forms.CharField(
+ label='Content',
+ widget=forms.Textarea,
+ initial=content
+ )
+
+ return GenericContentForm()
def get_name(self, view):
- return get_view_name(view.__class__, getattr(view, 'suffix', None))
+ return view.get_view_name()
def get_description(self, view):
- return get_view_description(view.__class__, html=True)
+ return view.get_view_description(html=True)
def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path)
- def render(self, data, accepted_media_type=None, renderer_context=None):
+ def get_context(self, data, accepted_media_type, renderer_context):
"""
- Render the HTML for the browsable API representation.
+ Returns the context used to render.
"""
- accepted_media_type = accepted_media_type or ''
- renderer_context = renderer_context or {}
-
view = renderer_context['view']
request = renderer_context['request']
response = renderer_context['response']
- media_types = [parser.media_type for parser in view.parser_classes]
renderer = self.get_default_renderer(view)
- content = self.get_content(renderer, data, accepted_media_type, renderer_context)
- put_form = self._get_form(view, 'PUT', request)
- post_form = self._get_form(view, 'POST', request)
- patch_form = self._get_form(view, 'PATCH', request)
- delete_form = self._get_form(view, 'DELETE', request)
- options_form = self._get_form(view, 'OPTIONS', request)
-
- raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
- raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
- raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
+ raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request)
+ raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
+ raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
- name = self.get_name(view)
- description = self.get_description(view)
- breadcrumb_list = self.get_breadcrumbs(request)
+ response_headers = dict(response.items())
+ renderer_content_type = ''
+ if renderer:
+ renderer_content_type = '%s' % renderer.media_type
+ if renderer.charset:
+ renderer_content_type += ' ;%s' % renderer.charset
+ response_headers['Content-Type'] = renderer_content_type
- template = loader.get_template(self.template)
- context = RequestContext(request, {
- 'content': content,
+ if hasattr(view, 'paginator') and view.paginator.display_page_controls:
+ paginator = view.paginator
+ else:
+ paginator = None
+
+ context = {
+ 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view,
'request': request,
'response': response,
- 'description': description,
- 'name': name,
+ 'description': self.get_description(view),
+ 'name': self.get_name(view),
'version': VERSION,
- 'breadcrumblist': breadcrumb_list,
+ 'paginator': paginator,
+ 'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
- 'available_formats': [renderer.format for renderer in view.renderer_classes],
+ 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
+ 'response_headers': response_headers,
- 'put_form': put_form,
- 'post_form': post_form,
- 'patch_form': patch_form,
- 'delete_form': delete_form,
- 'options_form': options_form,
+ 'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
+ 'post_form': self.get_rendered_html_form(data, view, 'POST', request),
+ 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
+ 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
'raw_data_patch_form': raw_data_patch_form,
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
- 'api_settings': api_settings
- })
+ 'display_edit_forms': bool(response.status_code != 403),
+ 'api_settings': api_settings
+ }
+ return context
+
+ def render(self, data, accepted_media_type=None, renderer_context=None):
+ """
+ Render the HTML for the browsable API representation.
+ """
+ self.accepted_media_type = accepted_media_type or ''
+ self.renderer_context = renderer_context or {}
+
+ template = loader.get_template(self.template)
+ context = self.get_context(data, accepted_media_type, renderer_context)
+ context = RequestContext(renderer_context['request'], context)
ret = template.render(context)
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include
# the normal deletion response code in the output)
+ response = renderer_context['response']
if response.status_code == status.HTTP_204_NO_CONTENT:
response.status_code = status.HTTP_200_OK
@@ -578,7 +652,7 @@ class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
charset = 'utf-8'
- BOUNDARY = 'BoUnDaRyStRiNg'
+ BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data)
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 919716f49..e4b5bc263 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header,
- and available as `request.DATA`
+ and available as `request.data`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
@@ -12,11 +12,13 @@ from __future__ import unicode_literals
from django.conf import settings
from django.http import QueryDict
from django.http.multipartparser import parse_header
+from django.utils import six
from django.utils.datastructures import MultiValueDict
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
-from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings
+import sys
+import warnings
def is_form_media_type(media_type):
@@ -28,6 +30,36 @@ def is_form_media_type(media_type):
base_media_type == 'multipart/form-data')
+class override_method(object):
+ """
+ A context manager that temporarily overrides the method on a request,
+ additionally setting the `view.request` attribute.
+
+ Usage:
+
+ with override_method(view, request, 'POST') as request:
+ ... # Do stuff with `view` and `request`
+ """
+ def __init__(self, view, request, method):
+ self.view = view
+ self.request = request
+ self.method = method
+ self.action = getattr(view, 'action', None)
+
+ def __enter__(self):
+ self.view.request = clone_request(self.request, self.method)
+ if self.action is not None:
+ # For viewsets we also set the `.action` attribute.
+ action_map = getattr(self.view, 'action_map', {})
+ self.view.action = action_map.get(self.method.lower())
+ return self.view.request
+
+ def __exit__(self, *args, **kwarg):
+ self.view.request = self.request
+ if self.action is not None:
+ self.view.action = self.action
+
+
class Empty(object):
"""
Placeholder for unset attributes.
@@ -52,6 +84,7 @@ def clone_request(request, method):
parser_context=request.parser_context)
ret._data = request._data
ret._files = request._files
+ ret._full_data = request._full_data
ret._content_type = request._content_type
ret._stream = request._stream
ret._method = method
@@ -61,6 +94,14 @@ def clone_request(request, method):
ret._auth = request._auth
if hasattr(request, '_authenticator'):
ret._authenticator = request._authenticator
+ if hasattr(request, 'accepted_renderer'):
+ ret.accepted_renderer = request.accepted_renderer
+ if hasattr(request, 'accepted_media_type'):
+ ret.accepted_media_type = request.accepted_media_type
+ if hasattr(request, 'version'):
+ ret.version = request.version
+ if hasattr(request, 'versioning_scheme'):
+ ret.versioning_scheme = request.versioning_scheme
return ret
@@ -103,6 +144,7 @@ class Request(object):
self.parser_context = parser_context
self._data = Empty
self._files = Empty
+ self._full_data = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
@@ -156,12 +198,30 @@ class Request(object):
return self._stream
@property
- def QUERY_PARAMS(self):
+ def query_params(self):
"""
More semantically correct name for request.GET.
"""
return self._request.GET
+ @property
+ def QUERY_PARAMS(self):
+ """
+ Synonym for `.query_params`, for backwards compatibility.
+ """
+ warnings.warn(
+ "`request.QUERY_PARAMS` is deprecated. Use `request.query_params` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
+ return self._request.GET
+
+ @property
+ def data(self):
+ if not _hasattr(self, '_full_data'):
+ self._load_data_and_files()
+ return self._full_data
+
@property
def DATA(self):
"""
@@ -170,6 +230,11 @@ class Request(object):
Similar to usual behaviour of `request.POST`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
+ warnings.warn(
+ "`request.DATA` is deprecated. Use `request.data` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
if not _hasattr(self, '_data'):
self._load_data_and_files()
return self._data
@@ -182,6 +247,11 @@ class Request(object):
Similar to usual behaviour of `request.FILES`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
+ warnings.warn(
+ "`request.FILES` is deprecated. Use `request.data` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
if not _hasattr(self, '_files'):
self._load_data_and_files()
return self._files
@@ -200,10 +270,14 @@ class Request(object):
def user(self, value):
"""
Sets the user on the current request. This is necessary to maintain
- compatilbility with django.contrib.auth where the user proprety is
+ compatibility with django.contrib.auth where the user property is
set in the login and logout functions.
+
+ Note that we also set the user on Django's underlying `HttpRequest`
+ instance, ensuring that it is available to any middleware in the stack.
"""
self._user = value
+ self._request.user = value
@property
def auth(self):
@@ -222,6 +296,7 @@ class Request(object):
request, such as an authentication token.
"""
self._auth = value
+ self._request.auth = value
@property
def successful_authenticator(self):
@@ -235,13 +310,18 @@ class Request(object):
def _load_data_and_files(self):
"""
- Parses the request content into self.DATA and self.FILES.
+ Parses the request content into `self.data`.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
+ if self._files:
+ self._full_data = self._data.copy()
+ self._full_data.update(self._files)
+ else:
+ self._full_data = self._data
def _load_method_and_content_type(self):
"""
@@ -256,18 +336,20 @@ class Request(object):
if not _hasattr(self, '_method'):
self._method = self._request.method
- if self._method == 'POST':
- # Allow X-HTTP-METHOD-OVERRIDE header
- self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
- self._method)
+ # Allow X-HTTP-METHOD-OVERRIDE header
+ if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
+ self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
def _load_stream(self):
"""
Return the content body of the request, as a stream.
"""
try:
- content_length = int(self.META.get('CONTENT_LENGTH',
- self.META.get('HTTP_CONTENT_LENGTH')))
+ content_length = int(
+ self.META.get(
+ 'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
+ )
+ )
except (ValueError, TypeError):
content_length = 0
@@ -276,7 +358,7 @@ class Request(object):
elif hasattr(self._request, 'read'):
self._stream = self._request
else:
- self._stream = BytesIO(self.raw_post_data)
+ self._stream = six.BytesIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
@@ -291,28 +373,36 @@ class Request(object):
)
# We only need to use form overloading on form POST requests.
- if (not USE_FORM_OVERLOADING
- or self._request.method != 'POST'
- or not is_form_media_type(self._content_type)):
+ if (
+ self._request.method != 'POST' or
+ not USE_FORM_OVERLOADING or
+ not is_form_media_type(self._content_type)
+ ):
return
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
+ self._full_data = self._data.copy()
+ self._full_data.update(self._files)
# Method overloading - change the method and remove the param from the content.
- if (self._METHOD_PARAM and
- self._METHOD_PARAM in self._data):
+ if (
+ self._METHOD_PARAM and
+ self._METHOD_PARAM in self._data
+ ):
self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse.
- if (self._CONTENT_PARAM and
+ if (
+ self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
- self._CONTENTTYPE_PARAM in self._data):
+ self._CONTENTTYPE_PARAM in self._data
+ ):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
- self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING))
- self._data, self._files = (Empty, Empty)
+ self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
+ self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self):
"""
@@ -324,7 +414,7 @@ class Request(object):
media_type = self.content_type
if stream is None or media_type is None:
- empty_data = QueryDict('', self._request._encoding)
+ empty_data = QueryDict('', encoding=self._request._encoding)
empty_files = MultiValueDict()
return (empty_data, empty_files)
@@ -333,7 +423,17 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
- parsed = parser.parse(stream, media_type, self.parser_context)
+ try:
+ parsed = parser.parse(stream, media_type, self.parser_context)
+ except:
+ # If we get an exception during parsing, fill in empty data and
+ # re-raise. Ensures we don't simply repeat the error when
+ # attempting to render the browsable renderer response, or when
+ # logging the request or similar.
+ self._data = QueryDict('', encoding=self._request._encoding)
+ self._files = MultiValueDict()
+ self._full_data = self._data
+ raise
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
@@ -356,9 +456,9 @@ class Request(object):
self._not_authenticated()
raise
- if not user_auth_tuple is None:
+ if user_auth_tuple is not None:
self._authenticator = authenticator
- self._user, self._auth = user_auth_tuple
+ self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
@@ -373,17 +473,25 @@ class Request(object):
self._authenticator = None
if api_settings.UNAUTHENTICATED_USER:
- self._user = api_settings.UNAUTHENTICATED_USER()
+ self.user = api_settings.UNAUTHENTICATED_USER()
else:
- self._user = None
+ self.user = None
if api_settings.UNAUTHENTICATED_TOKEN:
- self._auth = api_settings.UNAUTHENTICATED_TOKEN()
+ self.auth = api_settings.UNAUTHENTICATED_TOKEN()
else:
- self._auth = None
+ self.auth = None
- def __getattr__(self, attr):
+ def __getattribute__(self, attr):
"""
- Proxy other attributes to the underlying HttpRequest object.
+ If an attribute does not exist on this instance, then we also attempt
+ to proxy it to the underlying HttpRequest object.
"""
- return getattr(self._request, attr)
+ try:
+ return super(Request, self).__getattribute__(attr)
+ except AttributeError:
+ info = sys.exc_info()
+ try:
+ return getattr(self._request, attr)
+ except AttributeError:
+ six.reraise(info[0], info[1], info[2].tb_next)
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 5877c8a3e..c21c60a2e 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -7,7 +7,7 @@ The appropriate renderer is called during Django's template response rendering.
from __future__ import unicode_literals
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse
-from rest_framework.compat import six
+from django.utils import six
class Response(SimpleTemplateResponse):
@@ -16,7 +16,7 @@ class Response(SimpleTemplateResponse):
arbitrary media types.
"""
- def __init__(self, data=None, status=200,
+ def __init__(self, data=None, status=None,
template_name=None, headers=None,
exception=False, content_type=None):
"""
@@ -58,9 +58,15 @@ class Response(SimpleTemplateResponse):
ret = renderer.render(self.data, media_type, context)
if isinstance(ret, six.text_type):
- assert charset, 'renderer returned unicode, and did not specify ' \
- 'a charset value.'
+ assert charset, (
+ 'renderer returned unicode, and did not specify '
+ 'a charset value.'
+ )
return bytes(ret.encode(charset))
+
+ if not ret:
+ del self['Content-Type']
+
return ret
@property
@@ -75,10 +81,14 @@ class Response(SimpleTemplateResponse):
def __getstate__(self):
"""
- Remove attributes from the response that shouldn't be cached
+ Remove attributes from the response that shouldn't be cached.
"""
state = super(Response, self).__getstate__()
- for key in ('accepted_renderer', 'renderer_context', 'data'):
+ for key in (
+ 'accepted_renderer', 'renderer_context', 'resolver_match',
+ 'client', 'request', 'wsgi_request'
+ ):
if key in state:
del state[key]
+ state['_closable_objects'] = []
return state
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index a51b07f54..a251d99d6 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -1,12 +1,25 @@
"""
-Provide reverse functions that return fully qualified URLs
+Provide urlresolver functions that return fully qualified URLs or view names
"""
from __future__ import unicode_literals
from django.core.urlresolvers import reverse as django_reverse
+from django.utils import six
from django.utils.functional import lazy
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ """
+ If versioning is being used then we pass any `reverse` calls through
+ to the versioning scheme instance, so that the resulting URL
+ can be modified if needed.
+ """
+ scheme = getattr(request, 'versioning_scheme', None)
+ if scheme is not None:
+ return scheme.reverse(viewname, args, kwargs, request, format, **extra)
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+
+def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
@@ -20,4 +33,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
return url
-reverse_lazy = lazy(reverse, str)
+reverse_lazy = lazy(reverse, six.text_type)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 930011d39..b1e39ff7d 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -17,15 +17,19 @@ from __future__ import unicode_literals
import itertools
from collections import namedtuple
+from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
-from rest_framework.compat import patterns, url
+from rest_framework.compat import get_resolver_match, OrderedDict
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
+DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs'])
+DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
def replace_methodname(format_string, methodname):
@@ -61,13 +65,13 @@ class BaseRouter(object):
If `base_name` is not specified, attempt to automatically determine
it from the viewset.
"""
- raise NotImplemented('get_default_base_name must be overridden')
+ raise NotImplementedError('get_default_base_name must be overridden')
def get_urls(self):
"""
Return a list of URL patterns, given the registered viewsets.
"""
- raise NotImplemented('get_urls must be overridden')
+ raise NotImplementedError('get_urls must be overridden')
@property
def urls(self):
@@ -88,6 +92,14 @@ class SimpleRouter(BaseRouter):
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
+ # Dynamically generated list routes.
+ # Generated using @list_route decorator
+ # on methods of the viewset.
+ DynamicListRoute(
+ url=r'^{prefix}/{methodname}{trailing_slash}$',
+ name='{basename}-{methodnamehyphen}',
+ initkwargs={}
+ ),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
@@ -100,13 +112,10 @@ class SimpleRouter(BaseRouter):
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
- # Dynamically generated routes.
- # Generated using @action or @link decorators on methods of the viewset.
- Route(
+ # Dynamically generated detail routes.
+ # Generated using @detail_route decorator on methods of the viewset.
+ DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
- mapping={
- '{httpmethod}': '{methodname}',
- },
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
@@ -121,16 +130,13 @@ class SimpleRouter(BaseRouter):
If `base_name` is not specified, attempt to automatically determine
it from the viewset.
"""
- model_cls = getattr(viewset, 'model', None)
queryset = getattr(viewset, 'queryset', None)
- if model_cls is None and queryset is not None:
- model_cls = queryset.model
- assert model_cls, '`base_name` argument not specified, and could ' \
+ assert queryset is not None, '`base_name` argument not specified, and could ' \
'not automatically determine the name from the viewset, as ' \
- 'it does not have a `.model` or `.queryset` attribute.'
+ 'it does not have a `.queryset` attribute.'
- return model_cls._meta.object_name.lower()
+ return queryset.model._meta.object_name.lower()
def get_routes(self, viewset):
"""
@@ -139,33 +145,50 @@ class SimpleRouter(BaseRouter):
Returns a list of the Route namedtuple.
"""
- known_actions = flatten([route.mapping.values() for route in self.routes])
+ known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)])
- # Determine any `@action` or `@link` decorated methods on the viewset
- dynamic_routes = []
+ # Determine any `@detail_route` or `@list_route` decorated methods on the viewset
+ detail_routes = []
+ list_routes = []
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None)
+ detail = getattr(attr, 'detail', True)
if httpmethods:
if methodname in known_actions:
- raise ImproperlyConfigured('Cannot use @action or @link decorator on '
- 'method "%s" as it is an existing route' % methodname)
+ raise ImproperlyConfigured('Cannot use @detail_route or @list_route '
+ 'decorators on method "%s" '
+ 'as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods]
- dynamic_routes.append((httpmethods, methodname))
+ if detail:
+ detail_routes.append((httpmethods, methodname))
+ else:
+ list_routes.append((httpmethods, methodname))
+
+ def _get_dynamic_routes(route, dynamic_routes):
+ ret = []
+ for httpmethods, methodname in dynamic_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ initkwargs = route.initkwargs.copy()
+ initkwargs.update(method_kwargs)
+ url_path = initkwargs.pop("url_path", None) or methodname
+ ret.append(Route(
+ url=replace_methodname(route.url, url_path),
+ mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
+ name=replace_methodname(route.name, url_path),
+ initkwargs=initkwargs,
+ ))
+
+ return ret
ret = []
for route in self.routes:
- if route.mapping == {'{httpmethod}': '{methodname}'}:
- # Dynamic routes (@link or @action decorator)
- for httpmethods, methodname in dynamic_routes:
- initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, methodname),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
- initkwargs=initkwargs,
- ))
+ if isinstance(route, DynamicDetailRoute):
+ # Dynamic detail routes (@detail_route decorator)
+ ret += _get_dynamic_routes(route, detail_routes)
+ elif isinstance(route, DynamicListRoute):
+ # Dynamic list routes (@list_route decorator)
+ ret += _get_dynamic_routes(route, list_routes)
else:
# Standard route
ret.append(route)
@@ -184,14 +207,27 @@ class SimpleRouter(BaseRouter):
bound_methods[method] = action
return bound_methods
- def get_lookup_regex(self, viewset):
+ def get_lookup_regex(self, viewset, lookup_prefix=''):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
+
+ Note that lookup_prefix is not used directly inside REST rest_framework
+ itself, but is required in order to nicely support nested router
+ implementations, such as drf-nested-routers.
+
+ https://github.com/alanjds/drf-nested-routers
"""
- base_regex = '(?P<{lookup_field}>[^/]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>{lookup_value})'
+ # Use `pk` as default field, unset set. Default regex should not
+ # consume `.json` style suffixes and should break at '/' boundaries.
lookup_field = getattr(viewset, 'lookup_field', 'pk')
- return base_regex.format(lookup_field=lookup_field)
+ lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
+ return base_regex.format(
+ lookup_prefix=lookup_prefix,
+ lookup_field=lookup_field,
+ lookup_value=lookup_value
+ )
def get_urls(self):
"""
@@ -236,7 +272,7 @@ class DefaultRouter(SimpleRouter):
"""
Return a view to use as the API root.
"""
- api_root_dict = {}
+ api_root_dict = OrderedDict()
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@@ -244,10 +280,22 @@ class DefaultRouter(SimpleRouter):
class APIRoot(views.APIView):
_ignore_model_permissions = True
- def get(self, request, format=None):
- ret = {}
+ def get(self, request, *args, **kwargs):
+ ret = OrderedDict()
+ namespace = get_resolver_match(request).namespace
for key, url_name in api_root_dict.items():
- ret[key] = reverse(url_name, request=request, format=format)
+ if namespace:
+ url_name = namespace + ':' + url_name
+ try:
+ ret[key] = reverse(
+ url_name,
+ request=request,
+ format=kwargs.get('format', None)
+ )
+ except NoReverseMatch:
+ # Don't bail out if eg. no list routes exist, only detail routes.
+ continue
+
return Response(ret)
return APIRoot.as_view()
diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py
deleted file mode 100755
index ce11b213e..000000000
--- a/rest_framework/runtests/runcoverage.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python
-"""
-Useful tool to run the test suite for rest_framework and generate a coverage report.
-"""
-
-# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
-# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
-# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
-import os
-import sys
-
-# fix sys path so we don't need to setup PYTHONPATH
-sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
-
-from coverage import coverage
-
-
-def main():
- """Run the tests for rest_framework and generate a coverage report."""
-
- cov = coverage()
- cov.erase()
- cov.start()
-
- from django.conf import settings
- from django.test.utils import get_runner
- TestRunner = get_runner(settings)
-
- if hasattr(TestRunner, 'func_name'):
- # Pre 1.2 test runners were just functions,
- # and did not support the 'failfast' option.
- import warnings
- warnings.warn(
- 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
- DeprecationWarning
- )
- failures = TestRunner(['tests'])
- else:
- test_runner = TestRunner()
- failures = test_runner.run_tests(['tests'])
- cov.stop()
-
- # Discover the list of all modules that we should test coverage for
- import rest_framework
-
- project_dir = os.path.dirname(rest_framework.__file__)
- cov_files = []
-
- for (path, dirs, files) in os.walk(project_dir):
- # Drop tests and runtests directories from the test coverage report
- if os.path.basename(path) in ['tests', 'runtests', 'migrations']:
- continue
-
- # Drop the compat and six modules from coverage, since we're not interested in the coverage
- # of modules which are specifically for resolving environment dependant imports.
- # (Because we'll end up getting different coverage reports for it for each environment)
- if 'compat.py' in files:
- files.remove('compat.py')
-
- if 'six.py' in files:
- files.remove('six.py')
-
- # Same applies to template tags module.
- # This module has to include branching on Django versions,
- # so it's never possible for it to have full coverage.
- if 'rest_framework.py' in files:
- files.remove('rest_framework.py')
-
- cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
-
- cov.report(cov_files)
- if '--html' in sys.argv:
- cov.html_report(cov_files, directory='coverage')
- sys.exit(failures)
-
-if __name__ == '__main__':
- main()
diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py
deleted file mode 100755
index da36d23fc..000000000
--- a/rest_framework/runtests/runtests.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
-# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
-# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
-import os
-import sys
-
-# fix sys path so we don't need to setup PYTHONPATH
-sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
-
-import django
-from django.conf import settings
-from django.test.utils import get_runner
-
-
-def usage():
- return """
- Usage: python runtests.py [UnitTestClass].[method]
-
- You can pass the Class name of the `UnitTestClass` you want to test.
-
- Append a method name if you only want to test a specific method of that class.
- """
-
-
-def main():
- TestRunner = get_runner(settings)
-
- test_runner = TestRunner()
- if len(sys.argv) == 2:
- test_case = '.' + sys.argv[1]
- elif len(sys.argv) == 1:
- test_case = ''
- else:
- print(usage())
- sys.exit(1)
- test_module_name = 'rest_framework.tests'
- if django.VERSION[0] == 1 and django.VERSION[1] < 6:
- test_module_name = 'tests'
-
- failures = test_runner.run_tests([test_module_name + test_case])
-
- sys.exit(failures)
-
-if __name__ == '__main__':
- main()
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
deleted file mode 100644
index b3702d0bf..000000000
--- a/rest_framework/runtests/settings.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Django settings for testproject project.
-
-DEBUG = True
-TEMPLATE_DEBUG = DEBUG
-DEBUG_PROPAGATE_EXCEPTIONS = True
-
-ALLOWED_HOSTS = ['*']
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
- 'NAME': 'sqlite.db', # Or path to database file if using sqlite3.
- 'USER': '', # Not used with sqlite3.
- 'PASSWORD': '', # Not used with sqlite3.
- 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
- 'PORT': '', # Set to empty string for default. Not used with sqlite3.
- }
-}
-
-CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- }
-}
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'Europe/London'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-uk'
-
-SITE_ID = 1
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = True
-
-# If you set this to False, Django will not format dates, numbers and
-# calendars according to the current locale
-USE_L10N = True
-
-# Absolute filesystem path to the directory that will hold user-uploaded files.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
-
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a
-# trailing slash if there is a path component (optional in other cases).
-# Examples: "http://media.lawrence.com", "http://example.com/media/"
-MEDIA_URL = ''
-
-# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
-# 'django.template.loaders.eggs.Loader',
-)
-
-MIDDLEWARE_CLASSES = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
-)
-
-ROOT_URLCONF = 'urls'
-
-TEMPLATE_DIRS = (
- # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
- # Always use forward slashes, even on Windows.
- # Don't forget to use absolute paths, not relative paths.
-)
-
-INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- # Uncomment the next line to enable the admin:
- # 'django.contrib.admin',
- # Uncomment the next line to enable admin documentation:
- # 'django.contrib.admindocs',
- 'rest_framework',
- 'rest_framework.authtoken',
- 'rest_framework.tests',
-)
-
-# OAuth is optional and won't work if there is no oauth_provider & oauth2
-try:
- import oauth_provider
- import oauth2
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'oauth_provider',
- )
-
-try:
- import provider
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'provider',
- 'provider.oauth2',
- )
-
-STATIC_URL = '/static/'
-
-PASSWORD_HASHERS = (
- 'django.contrib.auth.hashers.SHA1PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
- 'django.contrib.auth.hashers.BCryptPasswordHasher',
- 'django.contrib.auth.hashers.MD5PasswordHasher',
- 'django.contrib.auth.hashers.CryptPasswordHasher',
-)
-
-AUTH_USER_MODEL = 'auth.User'
-
-import django
-
-if django.VERSION < (1, 3):
- INSTALLED_APPS += ('staticfiles',)
-
-
-# If we're running on the Jenkins server we want to archive the coverage reports as XML.
-import os
-if os.environ.get('HUDSON_URL', None):
- TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
- TEST_OUTPUT_VERBOSE = True
- TEST_OUTPUT_DESCRIPTIONS = True
- TEST_OUTPUT_DIR = 'xmlrunner'
diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py
deleted file mode 100644
index ed5baeae6..000000000
--- a/rest_framework/runtests/urls.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Blank URLConf just to keep runtests.py happy.
-"""
-from rest_framework.compat import patterns
-
-urlpatterns = patterns('',
-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 682a99a47..2eef6eeb5 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -6,20 +6,31 @@ form encoded input.
Serialization in REST framework is a two-phase process:
1. Serializers marshal between complex types like model instances, and
-python primatives.
-2. The process of marshalling between python primatives and request and
+python primitives.
+2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
-import copy
-import datetime
-import types
-from decimal import Decimal
-from django.core.paginator import Page
from django.db import models
-from django.forms import widgets
-from django.utils.datastructures import SortedDict
-from rest_framework.compat import get_concrete_model, six
+from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
+from django.db.models import query
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import postgres_fields, unicode_to_repr
+from rest_framework.utils import model_meta
+from rest_framework.utils.field_mapping import (
+ get_url_kwargs, get_field_kwargs,
+ get_relation_kwargs, get_nested_relation_kwargs,
+ ClassLookupDict
+)
+from rest_framework.utils.serializer_helpers import (
+ ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict
+)
+from rest_framework.validators import (
+ UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
+ UniqueTogetherValidator
+)
+import warnings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -28,941 +39,1354 @@ from rest_framework.compat import get_concrete_model, six
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
-from rest_framework.relations import *
-from rest_framework.fields import *
+from rest_framework.relations import * # NOQA
+from rest_framework.fields import * # NOQA
-class NestedValidationError(ValidationError):
+# We assume that 'validators' are intended for the child serializer,
+# rather than the parent serializer.
+LIST_SERIALIZER_KWARGS = (
+ 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
+ 'label', 'help_text', 'style', 'error_messages',
+ 'instance', 'data', 'partial', 'context'
+)
+
+
+# BaseSerializer
+# --------------
+
+class BaseSerializer(Field):
"""
- The default ValidationError behavior is to stringify each item in the list
- if the messages are a list of error messages.
+ The BaseSerializer class provides a minimal class which may be used
+ for writing custom serializer implementations.
- In the case of nested serializers, where the parent has many children,
- then the child's `serializer.errors` will be a list of dicts. In the case
- of a single child, the `serializer.errors` will be a dict.
+ Note that we strongly restrict the ordering of operations/properties
+ that may be used on the serializer in order to enforce correct usage.
- We need to override the default behavior to get properly nested error dicts.
+ In particular, if a `data=` argument is passed then:
+
+ .is_valid() - Available.
+ .initial_data - Available.
+ .validated_data - Only available after calling `is_valid()`
+ .errors - Only available after calling `is_valid()`
+ .data - Only available after calling `is_valid()`
+
+ If a `data=` argument is not passed then:
+
+ .is_valid() - Not available.
+ .initial_data - Not available.
+ .validated_data - Not available.
+ .errors - Not available.
+ .data - Available.
"""
- def __init__(self, message):
- if isinstance(message, dict):
- self.messages = [message]
- else:
- self.messages = message
-
-
-class DictWithMetadata(dict):
- """
- A dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overridden to remove the metadata from the dict, since it shouldn't be
- pickled and may in some instances be unpickleable.
- """
- return dict(self)
-
-
-class SortedDictWithMetadata(SortedDict):
- """
- A sorted dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overriden to remove the metadata from the dict, since it shouldn't be
- pickle and may in some instances be unpickleable.
- """
- return SortedDict(self).__dict__
-
-
-def _is_protected_type(obj):
- """
- True if the object is a native datatype that does not need to
- be serialized further.
- """
- return isinstance(obj, (
- types.NoneType,
- int, long,
- datetime.datetime, datetime.date, datetime.time,
- float, Decimal,
- basestring)
- )
-
-
-def _get_declared_fields(bases, attrs):
- """
- Create a list of serializer field instances from the passed in 'attrs',
- plus any fields on the base classes (in 'bases').
-
- Note that all fields from the base classes are used.
- """
- fields = [(field_name, attrs.pop(field_name))
- for field_name, obj in list(six.iteritems(attrs))
- if isinstance(obj, Field)]
- fields.sort(key=lambda x: x[1].creation_counter)
-
- # If this class is subclassing another Serializer, add that Serializer's
- # fields. Note that we loop over the bases in *reverse*. This is necessary
- # in order to maintain the correct order of fields.
- for base in bases[::-1]:
- if hasattr(base, 'base_fields'):
- fields = list(base.base_fields.items()) + fields
-
- return SortedDict(fields)
-
-
-class SerializerMetaclass(type):
- def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = _get_declared_fields(bases, attrs)
- return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
-
-
-class SerializerOptions(object):
- """
- Meta class options for Serializer
- """
- def __init__(self, meta):
- self.depth = getattr(meta, 'depth', 0)
- self.fields = getattr(meta, 'fields', ())
- self.exclude = getattr(meta, 'exclude', ())
-
-
-class BaseSerializer(WritableField):
- """
- This is the Serializer implementation.
- We need to implement it as `BaseSerializer` due to metaclass magicks.
- """
- class Meta(object):
- pass
-
- _options_class = SerializerOptions
- _dict_class = SortedDictWithMetadata
-
- def __init__(self, instance=None, data=None, files=None,
- context=None, partial=False, many=None,
- allow_add_remove=False, **kwargs):
+ def __init__(self, instance=None, data=empty, **kwargs):
+ self.instance = instance
+ if data is not empty:
+ self.initial_data = data
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
+ kwargs.pop('many', None)
super(BaseSerializer, self).__init__(**kwargs)
- self.opts = self._options_class(self.Meta)
- self.parent = None
- self.root = None
- self.partial = partial
- self.many = many
- self.allow_add_remove = allow_add_remove
- self.context = context or {}
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ListSerializer` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return cls.many_init(*args, **kwargs)
+ return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
- self.init_data = data
- self.init_files = files
- self.object = instance
- self.fields = self.get_fields()
-
- self._data = None
- self._files = None
- self._errors = None
- self._deleted = None
-
- if many and instance is not None and not hasattr(instance, '__iter__'):
- raise ValueError('instance should be a queryset or other iterable with many=True')
-
- if allow_add_remove and not many:
- raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
-
- #####
- # Methods to determine which fields to use when (de)serializing objects.
-
- def get_default_fields(self):
+ @classmethod
+ def many_init(cls, *args, **kwargs):
"""
- Return the complete set of default fields for the object, as a dict.
+ This method implements the creation of a `ListSerializer` parent
+ class when `many=True` is used. You can customize it if you need to
+ control which keyword arguments are passed to the parent, and
+ which are passed to the child.
+
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomListSerializer(*args, **kwargs)
"""
- return {}
+ child_serializer = cls(*args, **kwargs)
+ list_kwargs = {'child': child_serializer}
+ list_kwargs.update(dict([
+ (key, value) for key, value in kwargs.items()
+ if key in LIST_SERIALIZER_KWARGS
+ ]))
+ meta = getattr(cls, 'Meta', None)
+ list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
+ return list_serializer_class(*args, **list_kwargs)
- def get_fields(self):
- """
- Returns the complete set of fields for the object as a dict.
+ def to_internal_value(self, data):
+ raise NotImplementedError('`to_internal_value()` must be implemented.')
- This will be the set of any explicitly declared fields,
- plus the set of fields returned by get_default_fields().
- """
- ret = SortedDict()
+ def to_representation(self, instance):
+ raise NotImplementedError('`to_representation()` must be implemented.')
- # Get the explicitly declared fields
- base_fields = copy.deepcopy(self.base_fields)
- for key, field in base_fields.items():
- ret[key] = field
+ def update(self, instance, validated_data):
+ raise NotImplementedError('`update()` must be implemented.')
- # Add in the default fields
- default_fields = self.get_default_fields()
- for key, val in default_fields.items():
- if key not in ret:
- ret[key] = val
+ def create(self, validated_data):
+ raise NotImplementedError('`create()` must be implemented.')
- # If 'fields' is specified, use those fields, in that order.
- if self.opts.fields:
- assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
- new = SortedDict()
- for key in self.opts.fields:
- new[key] = ret[key]
- ret = new
+ def save(self, **kwargs):
+ assert not hasattr(self, 'save_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.save_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
- # Remove anything in 'exclude'
- if self.opts.exclude:
- assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
- for key in self.opts.exclude:
- ret.pop(key, None)
+ assert hasattr(self, '_errors'), (
+ 'You must call `.is_valid()` before calling `.save()`.'
+ )
- for key, field in ret.items():
- field.initialize(parent=self, field_name=key)
+ assert not self.errors, (
+ 'You cannot call `.save()` on a serializer with invalid data.'
+ )
- return ret
+ validated_data = dict(
+ list(self.validated_data.items()) +
+ list(kwargs.items())
+ )
- #####
- # Methods to convert or revert from objects <--> primitive representations.
-
- def get_field_key(self, field_name):
- """
- Return the key that should be used for a given field.
- """
- return field_name
-
- def restore_fields(self, data, files):
- """
- Core of deserialization, together with `restore_object`.
- Converts a dictionary of data into a dictionary of deserialized fields.
- """
- reverted_data = {}
-
- if data is not None and not isinstance(data, dict):
- self._errors['non_field_errors'] = ['Invalid data']
- return None
-
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- try:
- field.field_from_native(data, files, field_name, reverted_data)
- except ValidationError as err:
- self._errors[field_name] = list(err.messages)
-
- return reverted_data
-
- def perform_validation(self, attrs):
- """
- Run `validate_()` and `validate()` methods on the serializer
- """
- for field_name, field in self.fields.items():
- if field_name in self._errors:
- continue
- try:
- validate_method = getattr(self, 'validate_%s' % field_name, None)
- if validate_method:
- source = field.source or field_name
- attrs = validate_method(attrs, source)
- except ValidationError as err:
- self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
-
- # If there are already errors, we don't run .validate() because
- # field-validation failed and thus `attrs` may not be complete.
- # which in turn can cause inconsistent validation errors.
- if not self._errors:
- try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- if hasattr(err, 'message_dict'):
- for field_name, error_messages in err.message_dict.items():
- self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
- elif hasattr(err, 'messages'):
- self._errors['non_field_errors'] = err.messages
-
- return attrs
-
- def validate(self, attrs):
- """
- Stub method, to be overridden in Serializer subclasses
- """
- return attrs
-
- def restore_object(self, attrs, instance=None):
- """
- Deserialize a dictionary of attributes into an object instance.
- You should override this method to control how deserialized objects
- are instantiated.
- """
- if instance is not None:
- instance.update(attrs)
- return instance
- return attrs
-
- def to_native(self, obj):
- """
- Serialize objects -> primitives.
- """
- ret = self._dict_class()
- ret.fields = {}
-
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- key = self.get_field_key(field_name)
- value = field.field_to_native(obj, field_name)
- ret[key] = value
- ret.fields[key] = field
- return ret
-
- def from_native(self, data, files):
- """
- Deserialize primitives -> objects.
- """
- self._errors = {}
- if data is not None or files is not None:
- attrs = self.restore_fields(data, files)
- if attrs is not None:
- attrs = self.perform_validation(attrs)
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
else:
- self._errors['non_field_errors'] = ['No input provided']
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
- if not self._errors:
- return self.restore_object(attrs, instance=getattr(self, 'object', None))
+ return self.instance
- def field_to_native(self, obj, field_name):
- """
- Override default so that the serializer can be used as a nested field
- across relationships.
- """
- if self.source == '*':
- return self.to_native(obj)
+ def is_valid(self, raise_exception=False):
+ assert not hasattr(self, 'restore_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.restore_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
- try:
- source = self.source or field_name
- value = obj
+ assert hasattr(self, 'initial_data'), (
+ 'Cannot call `.is_valid()` as no `data=` keyword argument was '
+ 'passed when instantiating the serializer instance.'
+ )
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
- except ObjectDoesNotExist:
- return None
-
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
-
- if value is None:
- return None
-
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type))
-
- if many:
- return [self.to_native(item) for item in value]
- return self.to_native(value)
-
- def field_from_native(self, data, files, field_name, into):
- """
- Override default so that the serializer can be used as a writable
- nested field across relationships.
- """
- if self.read_only:
- return
-
- try:
- value = data[field_name]
- except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- value = copy.deepcopy(self.default)
+ if not hasattr(self, '_validated_data'):
+ try:
+ self._validated_data = self.run_validation(self.initial_data)
+ except ValidationError as exc:
+ self._validated_data = {}
+ self._errors = exc.detail
else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+ self._errors = {}
- # Set the serializer object if it exists
- obj = getattr(self.parent.object, field_name) if self.parent.object else None
+ if self._errors and raise_exception:
+ raise ValidationError(self._errors)
- if self.source == '*':
- if value:
- into.update(value)
- else:
- if value in (None, ''):
- into[(self.source or field_name)] = None
- else:
- kwargs = {
- 'instance': obj,
- 'data': value,
- 'context': self.context,
- 'partial': self.partial,
- 'many': self.many
- }
- serializer = self.__class__(**kwargs)
-
- if serializer.is_valid():
- into[self.source or field_name] = serializer.object
- else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
-
- def get_identity(self, data):
- """
- This hook is required for bulk update.
- It is used to determine the canonical identity of a given object.
-
- Note that the data has not been validated at this point, so we need
- to make sure that we catch any cases of incorrect datatypes being
- passed to this method.
- """
- try:
- return data.get('id', None)
- except AttributeError:
- return None
-
- @property
- def errors(self):
- """
- Run deserialization and return error data,
- setting self.object if no errors occurred.
- """
- if self._errors is None:
- data, files = self.init_data, self.init_files
-
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=3)
-
- if many:
- ret = []
- errors = []
- update = self.object is not None
-
- if update:
- # If this is a bulk update we need to map all the objects
- # to a canonical identity so we can determine which
- # individual object is being updated for each item in the
- # incoming data
- objects = self.object
- identities = [self.get_identity(self.to_native(obj)) for obj in objects]
- identity_to_objects = dict(zip(identities, objects))
-
- if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
- for item in data:
- if update:
- # Determine which object we're updating
- identity = self.get_identity(item)
- self.object = identity_to_objects.pop(identity, None)
- if self.object is None and not self.allow_add_remove:
- ret.append(None)
- errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
- continue
-
- ret.append(self.from_native(item, None))
- errors.append(self._errors)
-
- if update:
- self._deleted = identity_to_objects.values()
-
- self._errors = any(errors) and errors or []
- else:
- self._errors = {'non_field_errors': ['Expected a list of items.']}
- else:
- ret = self.from_native(data, files)
-
- if not self._errors:
- self.object = ret
-
- return self._errors
-
- def is_valid(self):
- return not self.errors
+ return not bool(self._errors)
@property
def data(self):
- """
- Returns the serialized data on the serializer.
- """
- if self._data is None:
- obj = self.object
+ if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
+ msg = (
+ 'When a serializer is passed a `data` keyword argument you '
+ 'must call `.is_valid()` before attempting to access the '
+ 'serialized `.data` representation.\n'
+ 'You should either call `.is_valid()` first, '
+ 'or access `.initial_data` instead.'
+ )
+ raise AssertionError(msg)
- if self.many is not None:
- many = self.many
+ if not hasattr(self, '_data'):
+ if self.instance is not None and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.instance)
+ elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.validated_data)
else:
- many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=2)
-
- if many:
- self._data = [self.to_native(item) for item in obj]
- else:
- self._data = self.to_native(obj)
-
+ self._data = self.get_initial()
return self._data
- def save_object(self, obj, **kwargs):
- obj.save(**kwargs)
+ @property
+ def errors(self):
+ if not hasattr(self, '_errors'):
+ msg = 'You must call `.is_valid()` before accessing `.errors`.'
+ raise AssertionError(msg)
+ return self._errors
- def delete_object(self, obj):
- obj.delete()
+ @property
+ def validated_data(self):
+ if not hasattr(self, '_validated_data'):
+ msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
+ raise AssertionError(msg)
+ return self._validated_data
+
+
+# Serializer & ListSerializer classes
+# -----------------------------------
+
+class SerializerMetaclass(type):
+ """
+ This metaclass sets a dictionary named `_declared_fields` on the class.
+
+ Any instances of `Field` included as attributes on either the class
+ or on any of its superclasses will be include in the
+ `_declared_fields` dictionary.
+ """
+
+ @classmethod
+ def _get_declared_fields(cls, bases, attrs):
+ fields = [(field_name, attrs.pop(field_name))
+ for field_name, obj in list(attrs.items())
+ if isinstance(obj, Field)]
+ fields.sort(key=lambda x: x[1]._creation_counter)
+
+ # If this class is subclassing another Serializer, add that Serializer's
+ # fields. Note that we loop over the bases in *reverse*. This is necessary
+ # in order to maintain the correct order of fields.
+ for base in reversed(bases):
+ if hasattr(base, '_declared_fields'):
+ fields = list(base._declared_fields.items()) + fields
+
+ return OrderedDict(fields)
+
+ def __new__(cls, name, bases, attrs):
+ attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
+ return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+
+
+def get_validation_error_detail(exc):
+ assert isinstance(exc, (ValidationError, DjangoValidationError))
+
+ if isinstance(exc, DjangoValidationError):
+ # Normally you should raise `serializers.ValidationError`
+ # inside your codebase, but we handle Django's validation
+ # exception class as well for simpler compat.
+ # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+ }
+ elif isinstance(exc.detail, dict):
+ # If errors may be a dict we use the standard {key: list of values}.
+ # Here we ensure that all the values are *lists* of errors.
+ return dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ])
+ elif isinstance(exc.detail, list):
+ # Errors raised as a list are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ }
+ # Errors raised as a string are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ }
+
+
+@six.add_metaclass(SerializerMetaclass)
+class Serializer(BaseSerializer):
+ default_error_messages = {
+ 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
+ }
+
+ @property
+ def fields(self):
+ """
+ A dictionary of {field_name: field_instance}.
+ """
+ # `fields` is evaluated lazily. We do this to ensure that we don't
+ # have issues importing modules that use ModelSerializers as fields,
+ # even if Django's app-loading stage has not yet run.
+ if not hasattr(self, '_fields'):
+ self._fields = BindingDict(self)
+ for key, value in self.get_fields().items():
+ self._fields[key] = value
+ return self._fields
+
+ def get_fields(self):
+ """
+ Returns a dictionary of {field_name: field_instance}.
+ """
+ # Every new serializer is created with a clone of the field instances.
+ # This allows users to dynamically modify the fields on a serializer
+ # instance without affecting every other serializer class.
+ return copy.deepcopy(self._declared_fields)
+
+ def get_validators(self):
+ """
+ Returns a list of validator callables.
+ """
+ # Used by the lazily-evaluated `validators` property.
+ meta = getattr(self, 'Meta', None)
+ validators = getattr(meta, 'validators', None)
+ return validators[:] if validators else []
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return OrderedDict([
+ (field_name, field.get_value(self.initial_data))
+ for field_name, field in self.fields.items()
+ if (field.get_value(self.initial_data) is not empty) and
+ not field.read_only
+ ])
+
+ return OrderedDict([
+ (field.field_name, field.get_initial())
+ for field in self.fields.values()
+ if not field.read_only
+ ])
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # nested HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
+
+ return value
+
+ def to_internal_value(self, data):
+ """
+ Dict of native values <- Dict of primitive datatypes.
+ """
+ if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
+ ret = OrderedDict()
+ errors = OrderedDict()
+ fields = [
+ field for field in self.fields.values()
+ if (not field.read_only) or (field.default is not empty)
+ ]
+
+ for field in fields:
+ validate_method = getattr(self, 'validate_' + field.field_name, None)
+ primitive_value = field.get_value(data)
+ try:
+ validated_value = field.run_validation(primitive_value)
+ if validate_method is not None:
+ validated_value = validate_method(validated_value)
+ except ValidationError as exc:
+ errors[field.field_name] = exc.detail
+ except DjangoValidationError as exc:
+ errors[field.field_name] = list(exc.messages)
+ except SkipField:
+ pass
+ else:
+ set_value(ret, field.source_attrs, validated_value)
+
+ if errors:
+ raise ValidationError(errors)
+
+ return ret
+
+ def to_representation(self, instance):
+ """
+ Object instance -> Dict of primitive datatypes.
+ """
+ ret = OrderedDict()
+ fields = [field for field in self.fields.values() if not field.write_only]
+
+ for field in fields:
+ try:
+ attribute = field.get_attribute(instance)
+ except SkipField:
+ continue
+
+ if attribute is None:
+ # We skip `to_representation` for `None` values so that
+ # fields do not have to explicitly deal with that case.
+ ret[field.field_name] = None
+ else:
+ ret[field.field_name] = field.to_representation(attribute)
+
+ return ret
+
+ def validate(self, attrs):
+ return attrs
+
+ def __repr__(self):
+ return unicode_to_repr(representation.serializer_repr(self, indent=1))
+
+ # The following are used for accessing `BoundField` instances on the
+ # serializer, for the purposes of presenting a form-like API onto the
+ # field values and field errors.
+
+ def __iter__(self):
+ for field in self.fields.values():
+ yield self[field.field_name]
+
+ def __getitem__(self, key):
+ field = self.fields[key]
+ value = self.data.get(key)
+ error = self.errors.get(key) if hasattr(self, '_errors') else None
+ if isinstance(field, Serializer):
+ return NestedBoundField(field, value, error)
+ return BoundField(field, value, error)
+
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
+
+ @property
+ def data(self):
+ ret = super(Serializer, self).data
+ return ReturnDict(ret, serializer=self)
+
+ @property
+ def errors(self):
+ ret = super(Serializer, self).errors
+ return ReturnDict(ret, serializer=self)
+
+
+# There's some replication of `ListField` here,
+# but that's probably better than obfuscating the call hierarchy.
+
+class ListSerializer(BaseSerializer):
+ child = None
+ many = True
+
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert self.child is not None, '`child` is a required argument.'
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListSerializer, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return self.to_representation(self.initial_data)
+ return []
+
+ def get_value(self, dictionary):
+ """
+ Given the input dictionary, return the field value.
+ """
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
+
+ return value
+
+ def to_internal_value(self, data):
+ """
+ List of dicts of native values <- List of dicts of primitive datatypes.
+ """
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
+
+ if not isinstance(data, list):
+ message = self.error_messages['not_a_list'].format(
+ input_type=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
+ ret = []
+ errors = []
+
+ for item in data:
+ try:
+ validated = self.child.run_validation(item)
+ except ValidationError as exc:
+ errors.append(exc.detail)
+ else:
+ ret.append(validated)
+ errors.append({})
+
+ if any(errors):
+ raise ValidationError(errors)
+
+ return ret
+
+ def to_representation(self, data):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ # Dealing with nested relationships, data can be a Manager,
+ # so, first get a queryset from the Manager if needed
+ iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data
+ return [
+ self.child.to_representation(item) for item in iterable
+ ]
+
+ def validate(self, attrs):
+ return attrs
+
+ def update(self, instance, validated_data):
+ raise NotImplementedError(
+ "Serializers with many=True do not support multiple update by "
+ "default, only multiple create. For updates it is unclear how to "
+ "deal with insertions and deletions. If you need to support "
+ "multiple update, use a `ListSerializer` class and override "
+ "`.update()` so you can specify the behavior exactly."
+ )
+
+ def create(self, validated_data):
+ return [
+ self.child.create(attrs) for attrs in validated_data
+ ]
def save(self, **kwargs):
"""
- Save the deserialized object and return it.
+ Save and return a list of object instances.
"""
- if isinstance(self.object, list):
- [self.save_object(item, **kwargs) for item in self.object]
+ validated_data = [
+ dict(list(attrs.items()) + list(kwargs.items()))
+ for attrs in self.validated_data
+ ]
+
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
else:
- self.save_object(self.object, **kwargs)
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
- if self.allow_add_remove and self._deleted:
- [self.delete_object(item) for item in self._deleted]
+ return self.instance
- return self.object
+ def __repr__(self):
+ return unicode_to_repr(representation.list_repr(self, indent=1))
- def metadata(self):
- """
- Return a dictionary of metadata about the fields on the serializer.
- Useful for things like responding to OPTIONS requests, or generating
- API schemas for auto-documentation.
- """
- return SortedDict(
- [(field_name, field.metadata())
- for field_name, field in six.iteritems(self.fields)]
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
+
+ @property
+ def data(self):
+ ret = super(ListSerializer, self).data
+ return ReturnList(ret, serializer=self)
+
+ @property
+ def errors(self):
+ ret = super(ListSerializer, self).errors
+ if isinstance(ret, dict):
+ return ReturnDict(ret, serializer=self)
+ return ReturnList(ret, serializer=self)
+
+
+# ModelSerializer & HyperlinkedModelSerializer
+# --------------------------------------------
+
+def raise_errors_on_nested_writes(method_name, serializer, validated_data):
+ """
+ Give explicit errors when users attempt to pass writable nested data.
+
+ If we don't do this explicitly they'd get a less helpful error when
+ calling `.save()` on the serializer.
+
+ We don't *automatically* support these sorts of nested writes because
+ there are too many ambiguities to define a default behavior.
+
+ Eg. Suppose we have a `UserSerializer` with a nested profile. How should
+ we handle the case of an update, where the `profile` relationship does
+ not exist? Any of the following might be valid:
+
+ * Raise an application error.
+ * Silently ignore the nested part of the update.
+ * Automatically create a profile instance.
+ """
+
+ # Ensure we don't have a writable nested field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # profile = ProfileSerializer()
+ assert not any(
+ isinstance(field, BaseSerializer) and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable nested'
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'nested serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
)
+ )
-
-class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
- pass
-
-
-class ModelSerializerOptions(SerializerOptions):
- """
- Meta class options for ModelSerializer
- """
- def __init__(self, meta):
- super(ModelSerializerOptions, self).__init__(meta)
- self.model = getattr(meta, 'model', None)
- self.read_only_fields = getattr(meta, 'read_only_fields', ())
+ # Ensure we don't have a writable dotted-source field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # address = serializer.CharField('profile.address')
+ assert not any(
+ '.' in field.source and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable dotted-source '
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'dotted-source serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
class ModelSerializer(Serializer):
"""
- A serializer that deals with model instances and querysets.
- """
- _options_class = ModelSerializerOptions
+ A `ModelSerializer` is just a regular `Serializer`, except that:
- field_mapping = {
+ * A set of default fields are automatically populated.
+ * A set of default validators are automatically populated.
+ * Default `.create()` and `.update()` implementations are provided.
+
+ The process of automatically determining a set of serializer fields
+ based on the model fields is reasonably complex, but you almost certainly
+ don't need to dig into the implementation.
+
+ If the `ModelSerializer` class *doesn't* generate the set of fields that
+ you need you should either declare the extra/differing fields explicitly on
+ the serializer class, or simply use a `Serializer` class.
+ """
+ serializer_field_mapping = {
models.AutoField: IntegerField,
- models.FloatField: FloatField,
- models.IntegerField: IntegerField,
- models.PositiveIntegerField: IntegerField,
- models.SmallIntegerField: IntegerField,
- models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
+ models.BigIntegerField: IntegerField,
+ models.BooleanField: BooleanField,
+ models.CharField: CharField,
+ models.CommaSeparatedIntegerField: CharField,
models.DateField: DateField,
- models.TimeField: TimeField,
+ models.DateTimeField: DateTimeField,
models.DecimalField: DecimalField,
models.EmailField: EmailField,
- models.CharField: CharField,
- models.URLField: URLField,
- models.SlugField: SlugField,
- models.TextField: CharField,
- models.CommaSeparatedIntegerField: CharField,
- models.BooleanField: BooleanField,
+ models.Field: ModelField,
models.FileField: FileField,
+ models.FloatField: FloatField,
models.ImageField: ImageField,
+ models.IntegerField: IntegerField,
+ models.NullBooleanField: NullBooleanField,
+ models.PositiveIntegerField: IntegerField,
+ models.PositiveSmallIntegerField: IntegerField,
+ models.SlugField: SlugField,
+ models.SmallIntegerField: IntegerField,
+ models.TextField: CharField,
+ models.TimeField: TimeField,
+ models.URLField: URLField,
}
+ serializer_related_field = PrimaryKeyRelatedField
+ serializer_url_field = HyperlinkedIdentityField
+ serializer_choice_field = ChoiceField
- def get_default_fields(self):
+ # Default `create` and `update` behavior...
+
+ def create(self, validated_data):
"""
- Return all the fields that should be serialized for the model.
+ We have a bit of extra checking around this in order to provide
+ descriptive messages when something goes wrong, but this method is
+ essentially just:
+
+ return ExampleModel.objects.create(**validated_data)
+
+ If there are many to many fields present on the instance then they
+ cannot be set until the model is instantiated, in which case the
+ implementation is like so:
+
+ example_relationship = validated_data.pop('example_relationship')
+ instance = ExampleModel.objects.create(**validated_data)
+ instance.example_relationship = example_relationship
+ return instance
+
+ The default implementation also does not handle nested relationships.
+ If you want to support writable nested relationships you'll need
+ to write an explicit `.create()` method.
"""
+ raise_errors_on_nested_writes('create', self, validated_data)
- cls = self.opts.model
- assert cls is not None, \
- "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
- opts = get_concrete_model(cls)._meta
- ret = SortedDict()
- nested = bool(self.opts.depth)
+ ModelClass = self.Meta.model
- # Deal with adding the primary key field
- pk_field = opts.pk
- while pk_field.rel and pk_field.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk
- pk_field = pk_field.rel.to._meta.pk
+ # Remove many-to-many relationships from validated_data.
+ # They are not valid arguments to the default `.create()` method,
+ # as they require that the instance has already been saved.
+ info = model_meta.get_field_info(ModelClass)
+ many_to_many = {}
+ for field_name, relation_info in info.relations.items():
+ if relation_info.to_many and (field_name in validated_data):
+ many_to_many[field_name] = validated_data.pop(field_name)
- field = self.get_pk_field(pk_field)
- if field:
- ret[pk_field.name] = field
+ try:
+ instance = ModelClass.objects.create(**validated_data)
+ except TypeError as exc:
+ msg = (
+ 'Got a `TypeError` when calling `%s.objects.create()`. '
+ 'This may be because you have a writable field on the '
+ 'serializer class that is not a valid argument to '
+ '`%s.objects.create()`. You may need to make the field '
+ 'read-only, or override the %s.create() method to handle '
+ 'this correctly.\nOriginal exception text was: %s.' %
+ (
+ ModelClass.__name__,
+ ModelClass.__name__,
+ self.__class__.__name__,
+ exc
+ )
+ )
+ raise TypeError(msg)
- # Deal with forward relationships
- forward_rels = [field for field in opts.fields if field.serialize]
- forward_rels += [field for field in opts.many_to_many if field.serialize]
+ # Save many-to-many relationships after the instance is created.
+ if many_to_many:
+ for field_name, value in many_to_many.items():
+ setattr(instance, field_name, value)
- for model_field in forward_rels:
- has_through_model = False
+ return instance
- if model_field.rel:
- to_many = isinstance(model_field,
- models.fields.related.ManyToManyField)
- related_model = model_field.rel.to
+ def update(self, instance, validated_data):
+ raise_errors_on_nested_writes('update', self, validated_data)
- if to_many and not model_field.rel.through._meta.auto_created:
- has_through_model = True
+ for attr, value in validated_data.items():
+ setattr(instance, attr, value)
+ instance.save()
- if model_field.rel and nested:
- if len(inspect.getargspec(self.get_nested_field).args) == 2:
- warnings.warn(
- 'The `get_nested_field(model_field)` call signature '
- 'is due to be deprecated. '
- 'Use `get_nested_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_nested_field(model_field)
- else:
- field = self.get_nested_field(model_field, related_model, to_many)
- elif model_field.rel:
- if len(inspect.getargspec(self.get_nested_field).args) == 3:
- warnings.warn(
- 'The `get_related_field(model_field, to_many)` call '
- 'signature is due to be deprecated. '
- 'Use `get_related_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_related_field(model_field, to_many=to_many)
- else:
- field = self.get_related_field(model_field, related_model, to_many)
- else:
- field = self.get_field(model_field)
+ return instance
- if field:
- if has_through_model:
- field.read_only = True
+ # Determine the fields to apply...
- ret[model_field.name] = field
+ def get_fields(self):
+ """
+ Return the dict of field names -> field instances that should be
+ used for `self.fields` when instantiating the serializer.
+ """
+ assert hasattr(self, 'Meta'), (
+ 'Class {serializer_class} missing "Meta" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+ assert hasattr(self.Meta, 'model'), (
+ 'Class {serializer_class} missing "Meta.model" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
- # Deal with reverse relationships
- if not self.opts.fields:
- reverse_rels = []
- else:
- # Reverse relationships are only included if they are explicitly
- # present in the `fields` option on the serializer
- reverse_rels = opts.get_all_related_objects()
- reverse_rels += opts.get_all_related_many_to_many_objects()
+ declared_fields = copy.deepcopy(self._declared_fields)
+ model = getattr(self.Meta, 'model')
+ depth = getattr(self.Meta, 'depth', 0)
- for relation in reverse_rels:
- accessor_name = relation.get_accessor_name()
- if not self.opts.fields or accessor_name not in self.opts.fields:
+ if depth is not None:
+ assert depth >= 0, "'depth' may not be negative."
+ assert depth <= 10, "'depth' may not be greater than 10."
+
+ # Retrieve metadata about fields & relationships on the model class.
+ info = model_meta.get_field_info(model)
+ field_names = self.get_field_names(declared_fields, info)
+
+ # Determine any extra field arguments and hidden fields that
+ # should be included
+ extra_kwargs = self.get_extra_kwargs()
+ extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
+ field_names, declared_fields, extra_kwargs
+ )
+
+ # Determine the fields that should be included on the serializer.
+ fields = OrderedDict()
+
+ for field_name in field_names:
+ # If the field is explicitly declared on the class then use that.
+ if field_name in declared_fields:
+ fields[field_name] = declared_fields[field_name]
continue
- related_model = relation.model
- to_many = relation.field.rel.multiple
- has_through_model = False
- is_m2m = isinstance(relation.field,
- models.fields.related.ManyToManyField)
- if is_m2m and not relation.field.rel.through._meta.auto_created:
- has_through_model = True
+ # Determine the serializer field class and keyword arguments.
+ field_class, field_kwargs = self.build_field(
+ field_name, info, model, depth
+ )
- if nested:
- field = self.get_nested_field(None, related_model, to_many)
+ # Include any kwargs defined in `Meta.extra_kwargs`
+ extra_field_kwargs = extra_kwargs.get(field_name, {})
+ field_kwargs = self.include_extra_kwargs(
+ field_kwargs, extra_field_kwargs
+ )
+
+ # Create the serializer field.
+ fields[field_name] = field_class(**field_kwargs)
+
+ # Add in any hidden fields.
+ fields.update(hidden_fields)
+
+ return fields
+
+ # Methods for determining the set of field names to include...
+
+ def get_field_names(self, declared_fields, info):
+ """
+ Returns the list of all field names that should be created when
+ instantiating this serializer class. This is based on the default
+ set of fields, but also takes into account the `Meta.fields` or
+ `Meta.exclude` options if they have been specified.
+ """
+ fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
+
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
+
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
+
+ assert not (fields and exclude), (
+ "Cannot set both 'fields' and 'exclude' options on "
+ "serializer {serializer_class}.".format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
+ if fields is not None:
+ # Ensure that all declared fields have also been included in the
+ # `Meta.fields` option.
+
+ # Do not require any fields that are declared a parent class,
+ # in order to allow serializer subclasses to only include
+ # a subset of fields.
+ required_field_names = set(declared_fields)
+ for cls in self.__class__.__bases__:
+ required_field_names -= set(getattr(cls, '_declared_fields', []))
+
+ for field_name in required_field_names:
+ assert field_name in fields, (
+ "The field '{field_name}' was declared on serializer "
+ "{serializer_class}, but has not been included in the "
+ "'fields' option.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ return fields
+
+ # Use the default set of field names if `Meta.fields` is not specified.
+ fields = self.get_default_field_names(declared_fields, info)
+
+ if exclude is not None:
+ # If `Meta.exclude` is included, then remove those fields.
+ for field_name in exclude:
+ assert field_name in fields, (
+ "The field '{field_name}' was include on serializer "
+ "{serializer_class} in the 'exclude' option, but does "
+ "not match any model field.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ fields.remove(field_name)
+
+ return fields
+
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
+ """
+ return (
+ [model_info.pk.name] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
+
+ # Methods for constructing serializer fields...
+
+ def build_field(self, field_name, info, model_class, nested_depth):
+ """
+ Return a two tuple of (cls, kwargs) to build a serializer field with.
+ """
+ if field_name in info.fields_and_pk:
+ model_field = info.fields_and_pk[field_name]
+ return self.build_standard_field(field_name, model_field)
+
+ elif field_name in info.relations:
+ relation_info = info.relations[field_name]
+ if not nested_depth:
+ return self.build_relational_field(field_name, relation_info)
else:
- field = self.get_related_field(None, related_model, to_many)
+ return self.build_nested_field(field_name, relation_info, nested_depth)
- if field:
- if has_through_model:
- field.read_only = True
+ elif hasattr(model_class, field_name):
+ return self.build_property_field(field_name, model_class)
- ret[accessor_name] = field
+ elif field_name == api_settings.URL_FIELD_NAME:
+ return self.build_url_field(field_name, model_class)
- # Add the `read_only` flag to any fields that have bee specified
- # in the `read_only_fields` option
- for field_name in self.opts.read_only_fields:
- assert field_name not in self.base_fields.keys(), \
- "field '%s' on serializer '%s' specfied in " \
- "`read_only_fields`, but also added " \
- "as an explict field. Remove it from `read_only_fields`." % \
- (field_name, self.__class__.__name__)
- assert field_name in ret, \
- "Noexistant field '%s' specified in `read_only_fields` " \
- "on serializer '%s'." % \
- (field_name, self.__class__.__name__)
- ret[field_name].read_only = True
+ return self.build_unknown_field(field_name, model_class)
- return ret
-
- def get_pk_field(self, model_field):
+ def build_standard_field(self, field_name, model_field):
"""
- Returns a default instance of the pk field.
+ Create regular model fields.
"""
- return self.get_field(model_field)
+ field_mapping = ClassLookupDict(self.serializer_field_mapping)
- def get_nested_field(self, model_field, related_model, to_many):
- """
- Creates a default instance of a nested relational field.
+ field_class = field_mapping[model_field]
+ field_kwargs = get_field_kwargs(field_name, model_field)
- Note that model_field will be `None` for reverse relationships.
+ if 'choices' in field_kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_class = self.serializer_choice_field
+
+ if not issubclass(field_class, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ field_kwargs.pop('model_field', None)
+
+ if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField):
+ # `allow_blank` is only valid for textual fields.
+ field_kwargs.pop('allow_blank', None)
+
+ if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
+ # Populate the `child` argument on `ListField` instances generated
+ # for the PostgrSQL specfic `ArrayField`.
+ child_model_field = model_field.base_field
+ child_field_class, child_field_kwargs = self.build_standard_field(
+ 'child', child_model_field
+ )
+ field_kwargs['child'] = child_field_class(**child_field_kwargs)
+
+ return field_class, field_kwargs
+
+ def build_relational_field(self, field_name, relation_info):
"""
- class NestedModelSerializer(ModelSerializer):
+ Create fields for forward and reverse relationships.
+ """
+ field_class = self.serializer_related_field
+ field_kwargs = get_relation_kwargs(field_name, relation_info)
+
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_class, HyperlinkedRelatedField):
+ field_kwargs.pop('view_name', None)
+
+ return field_class, field_kwargs
+
+ def build_nested_field(self, field_name, relation_info, nested_depth):
+ """
+ Create nested fields for forward and reverse relationships.
+ """
+ class NestedSerializer(ModelSerializer):
class Meta:
- model = related_model
- depth = self.opts.depth - 1
+ model = relation_info.related_model
+ depth = nested_depth
- return NestedModelSerializer(many=to_many)
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- def get_related_field(self, model_field, related_model, to_many):
+ return field_class, field_kwargs
+
+ def build_property_field(self, field_name, model_class):
"""
- Creates a default instance of a flat relational field.
-
- Note that model_field will be `None` for reverse relationships.
+ Create a read only field for model methods and properties.
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
+ field_class = ReadOnlyField
+ field_kwargs = {}
- kwargs = {
- 'queryset': related_model._default_manager,
- 'many': to_many
- }
+ return field_class, field_kwargs
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
-
- return PrimaryKeyRelatedField(**kwargs)
-
- def get_field(self, model_field):
+ def build_url_field(self, field_name, model_class):
"""
- Creates a default instance of a basic non-relational field.
+ Create a field representing the object's own URL.
"""
- kwargs = {}
+ field_class = self.serializer_url_field
+ field_kwargs = get_url_kwargs(model_class)
- if model_field.null or model_field.blank:
- kwargs['required'] = False
+ return field_class, field_kwargs
- if isinstance(model_field, models.AutoField) or not model_field.editable:
- kwargs['read_only'] = True
-
- if model_field.has_default():
- kwargs['default'] = model_field.get_default()
-
- if issubclass(model_field.__class__, models.TextField):
- kwargs['widget'] = widgets.Textarea
-
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
-
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
-
- # TODO: TypedChoiceField?
- if model_field.flatchoices: # This ModelField contains choices
- kwargs['choices'] = model_field.flatchoices
- return ChoiceField(**kwargs)
-
- # put this below the ChoiceField because min_value isn't a valid initializer
- if issubclass(model_field.__class__, models.PositiveIntegerField) or\
- issubclass(model_field.__class__, models.PositiveSmallIntegerField):
- kwargs['min_value'] = 0
-
- attribute_dict = {
- models.CharField: ['max_length'],
- models.CommaSeparatedIntegerField: ['max_length'],
- models.DecimalField: ['max_digits', 'decimal_places'],
- models.EmailField: ['max_length'],
- models.FileField: ['max_length'],
- models.ImageField: ['max_length'],
- models.SlugField: ['max_length'],
- models.URLField: ['max_length'],
- }
-
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
- for attribute in attributes:
- kwargs.update({attribute: getattr(model_field, attribute)})
-
- try:
- return self.field_mapping[model_field.__class__](**kwargs)
- except KeyError:
- return ModelField(model_field=model_field, **kwargs)
-
- def get_validation_exclusions(self):
+ def build_unknown_field(self, field_name, model_class):
"""
- Return a list of field names to exclude from model validation.
+ Raise an error on any unknown fields.
"""
- cls = self.opts.model
- opts = get_concrete_model(cls)._meta
- exclusions = [field.name for field in opts.fields + opts.many_to_many]
- for field_name, field in self.fields.items():
- field_name = field.source or field_name
- if field_name in exclusions and not field.read_only:
- exclusions.remove(field_name)
- return exclusions
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model_class.__name__)
+ )
- def full_clean(self, instance):
+ def include_extra_kwargs(self, kwargs, extra_kwargs):
"""
- Perform Django's full_clean, and populate the `errors` dictionary
- if any validation errors occur.
-
- Note that we don't perform this inside the `.restore_object()` method,
- so that subclasses can override `.restore_object()`, and still get
- the full_clean validation checking.
+ Include any 'extra_kwargs' that have been included for this field,
+ possibly removing any incompatible existing keyword arguments.
"""
- try:
- instance.full_clean(exclude=self.get_validation_exclusions())
- except ValidationError as err:
- self._errors = err.message_dict
- return None
- return instance
+ if extra_kwargs.get('read_only', False):
+ for attr in [
+ 'required', 'default', 'allow_blank', 'allow_null',
+ 'min_length', 'max_length', 'min_value', 'max_value',
+ 'validators', 'queryset'
+ ]:
+ kwargs.pop(attr, None)
- def restore_object(self, attrs, instance=None):
+ if extra_kwargs.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
+ kwargs.update(extra_kwargs)
+
+ return kwargs
+
+ # Methods for determining additional keyword arguments to apply...
+
+ def get_extra_kwargs(self):
"""
- Restore the model instance.
+ Return a dictionary mapping field names to a dictionary of
+ additional keyword arguments.
"""
- m2m_data = {}
- related_data = {}
- meta = self.opts.model._meta
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
- # Reverse fk or one-to-one relations
- for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- related_data[field_name] = attrs.pop(field_name)
+ read_only_fields = getattr(self.Meta, 'read_only_fields', None)
+ if read_only_fields is not None:
+ for field_name in read_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['read_only'] = True
+ extra_kwargs[field_name] = kwargs
- # Reverse m2m relations
- for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- m2m_data[field_name] = attrs.pop(field_name)
+ # These are all pending deprecation.
+ write_only_fields = getattr(self.Meta, 'write_only_fields', None)
+ if write_only_fields is not None:
+ warnings.warn(
+ "The `Meta.write_only_fields` option is deprecated. "
+ "Use `Meta.extra_kwargs={: {'write_only': True}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ for field_name in write_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['write_only'] = True
+ extra_kwargs[field_name] = kwargs
- # Forward m2m relations
- for field in meta.many_to_many:
- if field.name in attrs:
- m2m_data[field.name] = attrs.pop(field.name)
+ view_name = getattr(self.Meta, 'view_name', None)
+ if view_name is not None:
+ warnings.warn(
+ "The `Meta.view_name` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['view_name'] = view_name
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
- # Update an existing instance...
- if instance is not None:
- for key, val in attrs.items():
- setattr(instance, key, val)
+ lookup_field = getattr(self.Meta, 'lookup_field', None)
+ if lookup_field is not None:
+ warnings.warn(
+ "The `Meta.lookup_field` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['lookup_field'] = lookup_field
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
- # ...or create a new instance
- else:
- instance = self.opts.model(**attrs)
+ return extra_kwargs
- # Any relations that cannot be set until we've
- # saved the model get hidden away on these
- # private attributes, so we can deal with them
- # at the point of save.
- instance._related_data = related_data
- instance._m2m_data = m2m_data
-
- return instance
-
- def from_native(self, data, files):
+ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
"""
- Override the default method to also include model field validation.
+ Return any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('dict of updated extra kwargs', 'mapping of hidden fields')
"""
- instance = super(ModelSerializer, self).from_native(data, files)
- if not self._errors:
- return self.full_clean(instance)
+ model = getattr(self.Meta, 'model')
+ model_fields = self._get_model_fields(
+ field_names, declared_fields, extra_kwargs
+ )
- def save_object(self, obj, **kwargs):
+ # Determine if we need any additional `HiddenField` or extra keyword
+ # arguments to deal with `unique_for` dates that are required to
+ # be in the input data in order to validate it.
+ unique_constraint_names = set()
+
+ for model_field in model_fields.values():
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
+ model_field.unique_for_date,
+ model_field.unique_for_month,
+ model_field.unique_for_year
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(field_names).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ hidden_fields = {}
+ uniqueness_extra_kwargs = {}
+
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is referred too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
+
+ if unique_constraint_name in model_fields:
+ # The corresponding field is present in the serializer
+ if default is empty:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
+ else:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
+
+ # Update `extra_kwargs` with any new options.
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
+ else:
+ extra_kwargs[key] = value
+
+ return extra_kwargs, hidden_fields
+
+ def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
"""
- Save the deserialized object and return it.
+ Returns all the model fields that are being mapped to by fields
+ on the serializer class.
+ Returned as a dict of 'model field name' -> 'model field'.
+ Used internally by `get_uniqueness_field_options`.
"""
- obj.save(**kwargs)
+ model = getattr(self.Meta, 'model')
+ model_fields = {}
- if getattr(obj, '_m2m_data', None):
- for accessor_name, object_list in obj._m2m_data.items():
- setattr(obj, accessor_name, object_list)
- del(obj._m2m_data)
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # If the field is declared on the serializer
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
- if getattr(obj, '_related_data', None):
- for accessor_name, related in obj._related_data.items():
- setattr(obj, accessor_name, related)
- del(obj._related_data)
+ if '.' in source or source == '*':
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ continue
+
+ try:
+ field = model._meta.get_field(source)
+ if isinstance(field, DjangoModelField):
+ model_fields[source] = field
+ except FieldDoesNotExist:
+ pass
+
+ return model_fields
+
+ # Determine the validators to apply...
+
+ def get_validators(self):
+ """
+ Determine the set of validators to use when instantiating serializer.
+ """
+ # If the validators have been declared explicitly then use that.
+ validators = getattr(getattr(self, 'Meta', None), 'validators', None)
+ if validators is not None:
+ return validators[:]
+
+ # Otherwise use the default set of validators.
+ return (
+ self.get_unique_together_validators() +
+ self.get_unique_for_date_validators()
+ )
+
+ def get_unique_together_validators(self):
+ """
+ Determine a default set of validators for any unique_together contraints.
+ """
+ model_class_inheritance_tree = (
+ [self.Meta.model] +
+ list(self.Meta.model._meta.parents.keys())
+ )
+
+ # The field names we're passing though here only include fields
+ # which may map onto a model field. Any dotted field name lookups
+ # cannot map to a field, and must be a traversal, so we're not
+ # including those.
+ field_names = set([
+ field.source for field in self.fields.values()
+ if (field.source != '*') and ('.' not in field.source)
+ ])
+
+ # Note that we make sure to check `unique_together` both on the
+ # base model class, but also on any parent classes.
+ validators = []
+ for parent_class in model_class_inheritance_tree:
+ for unique_together in parent_class._meta.unique_together:
+ if field_names.issuperset(set(unique_together)):
+ validator = UniqueTogetherValidator(
+ queryset=parent_class._default_manager,
+ fields=unique_together
+ )
+ validators.append(validator)
+ return validators
+
+ def get_unique_for_date_validators(self):
+ """
+ Determine a default set of validators for the following contraints:
+
+ * unique_for_date
+ * unique_for_month
+ * unique_for_year
+ """
+ info = model_meta.get_field_info(self.Meta.model)
+ default_manager = self.Meta.model._default_manager
+ field_names = [field.source for field in self.fields.values()]
+
+ validators = []
+
+ for field_name, field in info.fields_and_pk.items():
+ if field.unique_for_date and field_name in field_names:
+ validator = UniqueForDateValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_date
+ )
+ validators.append(validator)
+
+ if field.unique_for_month and field_name in field_names:
+ validator = UniqueForMonthValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_month
+ )
+ validators.append(validator)
+
+ if field.unique_for_year and field_name in field_names:
+ validator = UniqueForYearValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_year
+ )
+ validators.append(validator)
+
+ return validators
-class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
- """
- Options for HyperlinkedModelSerializer
- """
- def __init__(self, meta):
- super(HyperlinkedModelSerializerOptions, self).__init__(meta)
- self.view_name = getattr(meta, 'view_name', None)
- self.lookup_field = getattr(meta, 'lookup_field', None)
+if hasattr(models, 'UUIDField'):
+ ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField
+
+if postgres_fields:
+ class CharMappingField(DictField):
+ child = CharField()
+
+ ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField
+ ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField
class HyperlinkedModelSerializer(ModelSerializer):
"""
- A subclass of ModelSerializer that uses hyperlinked relationships,
- instead of primary key relationships.
+ A type of `ModelSerializer` that uses hyperlinked relationships instead
+ of primary key relationships. Specifically:
+
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
"""
- _options_class = HyperlinkedModelSerializerOptions
- _default_view_name = '%(model_name)s-detail'
- _hyperlink_field_class = HyperlinkedRelatedField
+ serializer_related_field = HyperlinkedRelatedField
- def get_default_fields(self):
- fields = super(HyperlinkedModelSerializer, self).get_default_fields()
-
- if self.opts.view_name is None:
- self.opts.view_name = self._get_default_view_name(self.opts.model)
-
- if 'url' not in fields:
- url_field = HyperlinkedIdentityField(
- view_name=self.opts.view_name,
- lookup_field=self.opts.lookup_field
- )
- ret = self._dict_class()
- ret['url'] = url_field
- ret.update(fields)
- fields = ret
-
- return fields
-
- def get_pk_field(self, model_field):
- if self.opts.fields and model_field.name in self.opts.fields:
- return self.get_field(model_field)
-
- def get_related_field(self, model_field, related_model, to_many):
+ def get_default_field_names(self, declared_fields, model_info):
"""
- Creates a default instance of a flat relational field.
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'view_name': self._get_default_view_name(related_model),
- 'many': to_many
- }
+ return (
+ [api_settings.URL_FIELD_NAME] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
-
- if self.opts.lookup_field:
- kwargs['lookup_field'] = self.opts.lookup_field
-
- return self._hyperlink_field_class(**kwargs)
-
- def get_identity(self, data):
+ def build_nested_field(self, field_name, relation_info, nested_depth):
"""
- This hook is required for bulk update.
- We need to override the default, to use the url as the identity.
+ Create nested fields for forward and reverse relationships.
"""
- try:
- return data.get('url', None)
- except AttributeError:
- return None
+ class NestedSerializer(HyperlinkedModelSerializer):
+ class Meta:
+ model = relation_info.related_model
+ depth = nested_depth - 1
- def _get_default_view_name(self, model):
- """
- Return the view name to use if 'view_name' is not specified in 'Meta'
- """
- model_meta = model._meta
- format_kwargs = {
- 'app_label': model_meta.app_label,
- 'model_name': model_meta.object_name.lower()
- }
- return self._default_view_name % format_kwargs
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
+
+ return field_class, field_kwargs
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 8fd177d58..a3e9f5902 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -5,11 +5,11 @@ For example your project's `settings.py` file might look like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.TemplateHTMLRenderer',
)
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.TemplateHTMLRenderer',
)
}
@@ -18,13 +18,11 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
-
+from django.test.signals import setting_changed
from django.conf import settings
-from django.utils import importlib
-
+from django.utils import six
from rest_framework import ISO_8601
-from rest_framework.compat import six
-
+from rest_framework.compat import importlib
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
@@ -46,17 +44,13 @@ DEFAULTS = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
- 'DEFAULT_THROTTLE_CLASSES': (
- ),
+ 'DEFAULT_THROTTLE_CLASSES': (),
+ 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
+ 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
+ 'DEFAULT_VERSIONING_CLASS': None,
- 'DEFAULT_CONTENT_NEGOTIATION_CLASS':
- 'rest_framework.negotiation.DefaultContentNegotiation',
-
- # Genric view behavior
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.ModelSerializer',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
- 'rest_framework.pagination.PaginationSerializer',
+ # Generic view behavior
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_FILTER_BACKENDS': (),
# Throttling
@@ -64,15 +58,32 @@ DEFAULTS = {
'user': None,
'anon': None,
},
+ 'NUM_PROXIES': None,
# Pagination
- 'PAGINATE_BY': None,
- 'PAGINATE_BY_PARAM': None,
+ 'PAGE_SIZE': None,
+
+ # Filtering
+ 'SEARCH_PARAM': 'search',
+ 'ORDERING_PARAM': 'ordering',
+
+ # Versioning
+ 'DEFAULT_VERSION': None,
+ 'ALLOWED_VERSIONS': None,
+ 'VERSION_PARAM': 'version',
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
+ # View configuration
+ 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
+ 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
+
+ # Exception handling
+ 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
+ 'NON_FIELD_ERRORS_KEY': 'non_field_errors',
+
# Testing
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
@@ -88,25 +99,28 @@ DEFAULTS = {
'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format',
+ 'URL_FIELD_NAME': 'url',
# Input and output formats
- 'DATE_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATE_FORMAT': None,
+ 'DATE_FORMAT': ISO_8601,
+ 'DATE_INPUT_FORMATS': (ISO_8601,),
- 'DATETIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATETIME_FORMAT': None,
+ 'DATETIME_FORMAT': ISO_8601,
+ 'DATETIME_INPUT_FORMATS': (ISO_8601,),
- 'TIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'TIME_FORMAT': None,
+ 'TIME_FORMAT': ISO_8601,
+ 'TIME_INPUT_FORMATS': (ISO_8601,),
- # Pending deprecation
- 'FILTER_BACKEND': None,
+ # Encoding
+ 'UNICODE_JSON': True,
+ 'COMPACT_JSON': True,
+ 'COERCE_DECIMAL_TO_STRING': True,
+ 'UPLOADED_FILES_USE_URL': True,
+
+ # Pending deprecation:
+ 'PAGINATE_BY': None,
+ 'PAGINATE_BY_PARAM': None,
+ 'MAX_PAGINATE_BY': None
}
@@ -118,13 +132,16 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
- 'DEFAULT_MODEL_SERIALIZER_CLASS',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
+ 'DEFAULT_METADATA_CLASS',
+ 'DEFAULT_VERSIONING_CLASS',
+ 'DEFAULT_PAGINATION_CLASS',
'DEFAULT_FILTER_BACKENDS',
- 'FILTER_BACKEND',
+ 'EXCEPTION_HANDLER',
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
+ 'VIEW_NAME_FUNCTION',
+ 'VIEW_DESCRIPTION_FUNCTION'
)
@@ -133,7 +150,9 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
- if isinstance(val, six.string_types):
+ if val is None:
+ return None
+ elif isinstance(val, six.string_types):
return import_from_string(val, setting_name)
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
@@ -161,15 +180,15 @@ class APISettings(object):
For example:
from rest_framework.settings import api_settings
- print api_settings.DEFAULT_RENDERER_CLASSES
+ print(api_settings.DEFAULT_RENDERER_CLASSES)
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
"""
def __init__(self, user_settings=None, defaults=None, import_strings=None):
self.user_settings = user_settings or {}
- self.defaults = defaults or {}
- self.import_strings = import_strings or ()
+ self.defaults = defaults or DEFAULTS
+ self.import_strings = import_strings or IMPORT_STRINGS
def __getattr__(self, attr):
if attr not in self.defaults.keys():
@@ -186,15 +205,19 @@ class APISettings(object):
if val and attr in self.import_strings:
val = perform_import(val, attr)
- self.validate_setting(attr, val)
-
# Cache the result
setattr(self, attr, val)
return val
- def validate_setting(self, attr, val):
- if attr == 'FILTER_BACKEND' and val is not None:
- # Make sure we can initialize the class
- val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
+
+
+def reload_api_settings(*args, **kwargs):
+ global api_settings
+ setting, value = kwargs['setting'], kwargs['value']
+ if setting == 'REST_FRAMEWORK':
+ api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)
+
+
+setting_changed.connect(reload_api_settings)
diff --git a/rest_framework/six.py b/rest_framework/six.py
deleted file mode 100644
index 9e3823128..000000000
--- a/rest_framework/six.py
+++ /dev/null
@@ -1,389 +0,0 @@
-"""Utilities for writing code that runs on Python 2 and 3"""
-
-import operator
-import sys
-import types
-
-__author__ = "Benjamin Peterson "
-__version__ = "1.2.0"
-
-
-# True if we are running on Python 3.
-PY3 = sys.version_info[0] == 3
-
-if PY3:
- string_types = str,
- integer_types = int,
- class_types = type,
- text_type = str
- binary_type = bytes
-
- MAXSIZE = sys.maxsize
-else:
- string_types = basestring,
- integer_types = (int, long)
- class_types = (type, types.ClassType)
- text_type = unicode
- binary_type = str
-
- if sys.platform == "java":
- # Jython always uses 32 bits.
- MAXSIZE = int((1 << 31) - 1)
- else:
- # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
- class X(object):
- def __len__(self):
- return 1 << 31
- try:
- len(X())
- except OverflowError:
- # 32-bit
- MAXSIZE = int((1 << 31) - 1)
- else:
- # 64-bit
- MAXSIZE = int((1 << 63) - 1)
- del X
-
-
-def _add_doc(func, doc):
- """Add documentation to a function."""
- func.__doc__ = doc
-
-
-def _import_module(name):
- """Import module, returning the module after the last dot."""
- __import__(name)
- return sys.modules[name]
-
-
-class _LazyDescr(object):
-
- def __init__(self, name):
- self.name = name
-
- def __get__(self, obj, tp):
- result = self._resolve()
- setattr(obj, self.name, result)
- # This is a bit ugly, but it avoids running this again.
- delattr(tp, self.name)
- return result
-
-
-class MovedModule(_LazyDescr):
-
- def __init__(self, name, old, new=None):
- super(MovedModule, self).__init__(name)
- if PY3:
- if new is None:
- new = name
- self.mod = new
- else:
- self.mod = old
-
- def _resolve(self):
- return _import_module(self.mod)
-
-
-class MovedAttribute(_LazyDescr):
-
- def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
- super(MovedAttribute, self).__init__(name)
- if PY3:
- if new_mod is None:
- new_mod = name
- self.mod = new_mod
- if new_attr is None:
- if old_attr is None:
- new_attr = name
- else:
- new_attr = old_attr
- self.attr = new_attr
- else:
- self.mod = old_mod
- if old_attr is None:
- old_attr = name
- self.attr = old_attr
-
- def _resolve(self):
- module = _import_module(self.mod)
- return getattr(module, self.attr)
-
-
-
-class _MovedItems(types.ModuleType):
- """Lazy loading of moved objects"""
-
-
-_moved_attributes = [
- MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
- MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
- MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
- MovedAttribute("map", "itertools", "builtins", "imap", "map"),
- MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
- MovedAttribute("reduce", "__builtin__", "functools"),
- MovedAttribute("StringIO", "StringIO", "io"),
- MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
- MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
-
- MovedModule("builtins", "__builtin__"),
- MovedModule("configparser", "ConfigParser"),
- MovedModule("copyreg", "copy_reg"),
- MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
- MovedModule("http_cookies", "Cookie", "http.cookies"),
- MovedModule("html_entities", "htmlentitydefs", "html.entities"),
- MovedModule("html_parser", "HTMLParser", "html.parser"),
- MovedModule("http_client", "httplib", "http.client"),
- MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
- MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
- MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
- MovedModule("cPickle", "cPickle", "pickle"),
- MovedModule("queue", "Queue"),
- MovedModule("reprlib", "repr"),
- MovedModule("socketserver", "SocketServer"),
- MovedModule("tkinter", "Tkinter"),
- MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
- MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
- MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
- MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
- MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
- MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
- MovedModule("tkinter_colorchooser", "tkColorChooser",
- "tkinter.colorchooser"),
- MovedModule("tkinter_commondialog", "tkCommonDialog",
- "tkinter.commondialog"),
- MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_font", "tkFont", "tkinter.font"),
- MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
- MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
- "tkinter.simpledialog"),
- MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
- MovedModule("winreg", "_winreg"),
-]
-for attr in _moved_attributes:
- setattr(_MovedItems, attr.name, attr)
-del attr
-
-moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves")
-
-
-def add_move(move):
- """Add an item to six.moves."""
- setattr(_MovedItems, move.name, move)
-
-
-def remove_move(name):
- """Remove item from six.moves."""
- try:
- delattr(_MovedItems, name)
- except AttributeError:
- try:
- del moves.__dict__[name]
- except KeyError:
- raise AttributeError("no such move, %r" % (name,))
-
-
-if PY3:
- _meth_func = "__func__"
- _meth_self = "__self__"
-
- _func_code = "__code__"
- _func_defaults = "__defaults__"
-
- _iterkeys = "keys"
- _itervalues = "values"
- _iteritems = "items"
-else:
- _meth_func = "im_func"
- _meth_self = "im_self"
-
- _func_code = "func_code"
- _func_defaults = "func_defaults"
-
- _iterkeys = "iterkeys"
- _itervalues = "itervalues"
- _iteritems = "iteritems"
-
-
-try:
- advance_iterator = next
-except NameError:
- def advance_iterator(it):
- return it.next()
-next = advance_iterator
-
-
-if PY3:
- def get_unbound_function(unbound):
- return unbound
-
- Iterator = object
-
- def callable(obj):
- return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
-else:
- def get_unbound_function(unbound):
- return unbound.im_func
-
- class Iterator(object):
-
- def next(self):
- return type(self).__next__(self)
-
- callable = callable
-_add_doc(get_unbound_function,
- """Get the function out of a possibly unbound function""")
-
-
-get_method_function = operator.attrgetter(_meth_func)
-get_method_self = operator.attrgetter(_meth_self)
-get_function_code = operator.attrgetter(_func_code)
-get_function_defaults = operator.attrgetter(_func_defaults)
-
-
-def iterkeys(d):
- """Return an iterator over the keys of a dictionary."""
- return iter(getattr(d, _iterkeys)())
-
-def itervalues(d):
- """Return an iterator over the values of a dictionary."""
- return iter(getattr(d, _itervalues)())
-
-def iteritems(d):
- """Return an iterator over the (key, value) pairs of a dictionary."""
- return iter(getattr(d, _iteritems)())
-
-
-if PY3:
- def b(s):
- return s.encode("latin-1")
- def u(s):
- return s
- if sys.version_info[1] <= 1:
- def int2byte(i):
- return bytes((i,))
- else:
- # This is about 2x faster than the implementation above on 3.2+
- int2byte = operator.methodcaller("to_bytes", 1, "big")
- import io
- StringIO = io.StringIO
- BytesIO = io.BytesIO
-else:
- def b(s):
- return s
- def u(s):
- return unicode(s, "unicode_escape")
- int2byte = chr
- import StringIO
- StringIO = BytesIO = StringIO.StringIO
-_add_doc(b, """Byte literal""")
-_add_doc(u, """Text literal""")
-
-
-if PY3:
- import builtins
- exec_ = getattr(builtins, "exec")
-
-
- def reraise(tp, value, tb=None):
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
-
- print_ = getattr(builtins, "print")
- del builtins
-
-else:
- def exec_(code, globs=None, locs=None):
- """Execute code in a namespace."""
- if globs is None:
- frame = sys._getframe(1)
- globs = frame.f_globals
- if locs is None:
- locs = frame.f_locals
- del frame
- elif locs is None:
- locs = globs
- exec("""exec code in globs, locs""")
-
-
- exec_("""def reraise(tp, value, tb=None):
- raise tp, value, tb
-""")
-
-
- def print_(*args, **kwargs):
- """The new-style print function."""
- fp = kwargs.pop("file", sys.stdout)
- if fp is None:
- return
- def write(data):
- if not isinstance(data, basestring):
- data = str(data)
- fp.write(data)
- want_unicode = False
- sep = kwargs.pop("sep", None)
- if sep is not None:
- if isinstance(sep, unicode):
- want_unicode = True
- elif not isinstance(sep, str):
- raise TypeError("sep must be None or a string")
- end = kwargs.pop("end", None)
- if end is not None:
- if isinstance(end, unicode):
- want_unicode = True
- elif not isinstance(end, str):
- raise TypeError("end must be None or a string")
- if kwargs:
- raise TypeError("invalid keyword arguments to print()")
- if not want_unicode:
- for arg in args:
- if isinstance(arg, unicode):
- want_unicode = True
- break
- if want_unicode:
- newline = unicode("\n")
- space = unicode(" ")
- else:
- newline = "\n"
- space = " "
- if sep is None:
- sep = space
- if end is None:
- end = newline
- for i, arg in enumerate(args):
- if i:
- write(sep)
- write(arg)
- write(end)
-
-_add_doc(reraise, """Reraise an exception.""")
-
-
-def with_metaclass(meta, base=object):
- """Create a base class with a metaclass."""
- return meta("NewBase", (base,), {})
-
-
-### Additional customizations for Django ###
-
-if PY3:
- _iterlists = "lists"
- _assertRaisesRegex = "assertRaisesRegex"
-else:
- _iterlists = "iterlists"
- _assertRaisesRegex = "assertRaisesRegexp"
-
-
-def iterlists(d):
- """Return an iterator over the values of a MultiValueDict."""
- return getattr(d, _iterlists)()
-
-
-def assertRaisesRegex(self, *args, **kwargs):
- return getattr(self, _assertRaisesRegex)(*args, **kwargs)
-
-
-add_move(MovedModule("_dummy_thread", "dummy_thread"))
-add_move(MovedModule("_thread", "thread"))
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 6bfb778cc..04f12ed3d 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -6,30 +6,36 @@ a single block in the template.
*/
-
.form-actions {
- background: transparent;
- border-top-color: transparent;
- padding-top: 0;
+ background: transparent;
+ border-top-color: transparent;
+ padding-top: 0;
+ text-align: right;
+}
+
+#generic-content-form textarea {
+ font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+ font-size: 80%;
}
.navbar-inverse .brand a {
- color: #999;
+ color: #999999;
}
.navbar-inverse .brand:hover a {
- color: white;
- text-decoration: none;
+ color: white;
+ text-decoration: none;
}
/* custom navigation styles */
-.wrapper .navbar{
+.navbar {
width: 100%;
- position: absolute;
+ position: fixed;
left: 0;
top: 0;
+ z-index: 3;
}
-.navbar .navbar-inner{
+.navbar {
background: #2C2C2C;
color: white;
border: none;
@@ -37,149 +43,165 @@ a single block in the template.
border-radius: 0px;
}
-.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
+.navbar .nav li, .navbar .nav li a, .navbar .brand:hover {
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
- background: #2c2c2c;
+ background: #2C2C2C;
}
-.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
- color: #A30000;
+.navbar .dropdown-menu li a, .navbar .dropdown-menu li {
+ color: #A30000;
}
-.navbar .navbar-inner .dropdown-menu li a:hover{
- background: #eeeeee;
- color: #c20000;
+
+.navbar .dropdown-menu li a:hover {
+ background: #EEEEEE;
+ color: #C20000;
+}
+
+.pagination>.disabled>a,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>a:focus {
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.pager>.disabled>a,
+.pager>.disabled>a:hover,
+.pager>.disabled>a:focus {
+ pointer-events: none;
+}
+
+.pager .next {
+ margin-left: 10px;
}
/*=== dabapps bootstrap styles ====*/
-html{
+html {
width:100%;
background: none;
}
-body, .navbar .navbar-inner .container-fluid {
+/*body, .navbar .container-fluid {
max-width: 1150px;
margin: 0 auto;
-}
+}*/
-body{
+body {
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
-#content{
- margin: 0;
+#content {
+ margin: 0;
+ padding-bottom: 60px;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
+
.wrapper {
+ position: relative;
+ top: 0;
+ left: 0;
+ padding-top: 60px;
+ margin: -60px 0;
min-height: 100%;
- height: auto !important;
- height: 100%;
- margin: 0 auto -60px;
}
.form-switcher {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.well {
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
}
.well .form-actions {
- padding-bottom: 0;
- margin-bottom: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
}
.well form {
- margin-bottom: 0;
-}
-
-.well form .help-block {
- color: #999;
+ margin-bottom: 0;
}
.nav-tabs {
- border: 0;
+ border: 0;
}
.nav-tabs > li {
- float: right;
+ float: right;
}
.nav-tabs li a {
- margin-right: 0;
+ margin-right: 0;
}
.nav-tabs > .active > a {
- background: #f5f5f5;
+ background: #F5F5F5;
}
.nav-tabs > .active > a:hover {
- background: #f5f5f5;
+ background: #F5F5F5;
}
-.tabbable.first-tab-active .tab-content
-{
- border-top-right-radius: 0;
+.tabbable.first-tab-active .tab-content {
+ border-top-right-radius: 0;
}
-#footer, #push {
- height: 60px; /* .push must be the same height as .footer */
+footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ clear: both;
+ z-index: 10;
+ height: 60px;
+ width: 95%;
+ margin: 0 2.5%;
}
-#footer{
- text-align: right;
-}
-
-#footer p {
+footer p {
text-align: center;
color: gray;
- border-top: 1px solid #DDD;
+ border-top: 1px solid #DDDDDD;
padding-top: 10px;
}
-#footer a {
- color: gray;
+footer a {
+ color: gray !important;
font-weight: bold;
}
-#footer a:hover {
+footer a:hover {
color: gray;
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
+ margin: 0;
}
/* custom general page styles */
-.hero-unit h2, .hero-unit h1{
+.hero-unit h1, .hero-unit h2 {
color: #A30000;
}
-body a, body a{
+body a {
color: #A30000;
}
-body a:hover{
+body a:hover {
color: #c20000;
}
-#content a span{
- text-decoration: underline;
- }
-
.request-info {
- clear:both;
+ clear:both;
}
diff --git a/rest_framework/static/rest_framework/css/bootstrap.min.css b/rest_framework/static/rest_framework/css/bootstrap.min.css
index 373f4b430..a9f35ceed 100644
--- a/rest_framework/static/rest_framework/css/bootstrap.min.css
+++ b/rest_framework/static/rest_framework/css/bootstrap.min.css
@@ -1,841 +1,5 @@
/*!
- * Bootstrap v2.1.1
- *
- * Copyright 2012 Twitter, Inc
- * Licensed under the Apache License v2.0
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Designed and built with all the love in the world @twitter by @mdo and @fat.
- */
-.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;}
-.clearfix:after{clear:both;}
-.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;}
-.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}
-article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
-audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
-audio:not([controls]){display:none;}
-html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
-a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-a:hover,a:active{outline:0;}
-sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
-sup{top:-0.5em;}
-sub{bottom:-0.25em;}
-img{max-width:100%;width:auto\9;height:auto;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic;}
-#map_canvas img{max-width:none;}
-button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
-button,input{*overflow:visible;line-height:normal;}
-button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
-button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
-input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield;}
-input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
-textarea{overflow:auto;vertical-align:top;}
-body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333333;background-color:#ffffff;}
-a{color:#0088cc;text-decoration:none;}
-a:hover{color:#005580;text-decoration:underline;}
-.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);}
-.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px;}
-.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;}
-.row:after{clear:both;}
-[class*="span"]{float:left;min-height:1px;margin-left:20px;}
-.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
-.span12{width:940px;}
-.span11{width:860px;}
-.span10{width:780px;}
-.span9{width:700px;}
-.span8{width:620px;}
-.span7{width:540px;}
-.span6{width:460px;}
-.span5{width:380px;}
-.span4{width:300px;}
-.span3{width:220px;}
-.span2{width:140px;}
-.span1{width:60px;}
-.offset12{margin-left:980px;}
-.offset11{margin-left:900px;}
-.offset10{margin-left:820px;}
-.offset9{margin-left:740px;}
-.offset8{margin-left:660px;}
-.offset7{margin-left:580px;}
-.offset6{margin-left:500px;}
-.offset5{margin-left:420px;}
-.offset4{margin-left:340px;}
-.offset3{margin-left:260px;}
-.offset2{margin-left:180px;}
-.offset1{margin-left:100px;}
-.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;}
-.row-fluid:after{clear:both;}
-.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;}
-.row-fluid [class*="span"]:first-child{margin-left:0;}
-.row-fluid .span12{width:100%;*width:99.94680851063829%;}
-.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%;}
-.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%;}
-.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%;}
-.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%;}
-.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%;}
-.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%;}
-.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%;}
-.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%;}
-.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%;}
-.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%;}
-.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%;}
-.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%;}
-.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%;}
-.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%;}
-.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%;}
-.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%;}
-.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%;}
-.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%;}
-.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%;}
-.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%;}
-.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%;}
-.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%;}
-.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%;}
-.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%;}
-.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%;}
-.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%;}
-.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%;}
-.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%;}
-.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%;}
-.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%;}
-.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%;}
-.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%;}
-.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%;}
-.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%;}
-.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%;}
-[class*="span"].hide,.row-fluid [class*="span"].hide{display:none;}
-[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right;}
-.container{margin-right:auto;margin-left:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";line-height:0;}
-.container:after{clear:both;}
-.container-fluid{padding-right:20px;padding-left:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";line-height:0;}
-.container-fluid:after{clear:both;}
-p{margin:0 0 10px;}
-.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px;}
-small{font-size:85%;}
-strong{font-weight:bold;}
-em{font-style:italic;}
-cite{font-style:normal;}
-.muted{color:#999999;}
-.text-warning{color:#c09853;}
-.text-error{color:#b94a48;}
-.text-info{color:#3a87ad;}
-.text-success{color:#468847;}
-h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:1;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999999;}
-h1{font-size:36px;line-height:40px;}
-h2{font-size:30px;line-height:40px;}
-h3{font-size:24px;line-height:40px;}
-h4{font-size:18px;line-height:20px;}
-h5{font-size:14px;line-height:20px;}
-h6{font-size:12px;line-height:20px;}
-h1 small{font-size:24px;}
-h2 small{font-size:18px;}
-h3 small{font-size:14px;}
-h4 small{font-size:14px;}
-.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eeeeee;}
-ul,ol{padding:0;margin:0 0 10px 25px;}
-ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
-li{line-height:20px;}
-ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
-dl{margin-bottom:20px;}
-dt,dd{line-height:20px;}
-dt{font-weight:bold;}
-dd{margin-left:10px;}
-.dl-horizontal{*zoom:1;}.dl-horizontal:before,.dl-horizontal:after{display:table;content:"";line-height:0;}
-.dl-horizontal:after{clear:both;}
-.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
-.dl-horizontal dd{margin-left:180px;}
-hr{margin:20px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
-abbr[title]{cursor:help;border-bottom:1px dotted #999999;}
-abbr.initialism{font-size:90%;text-transform:uppercase;}
-blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px;}
-blockquote small{display:block;line-height:20px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
-blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eeeeee;border-left:0;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
-blockquote.pull-right small:before{content:'';}
-blockquote.pull-right small:after{content:'\00A0 \2014';}
-q:before,q:after,blockquote:before,blockquote:after{content:"";}
-address{display:block;margin-bottom:20px;font-style:normal;line-height:20px;}
-code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
-pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}pre.prettyprint{margin-bottom:20px;}
-pre code{padding:0;color:inherit;background-color:transparent;border:0;}
-.pre-scrollable{max-height:340px;overflow-y:scroll;}
-.label,.badge{font-size:11.844px;font-weight:bold;line-height:14px;color:#ffffff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;}
-.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
-a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
-.label-important,.badge-important{background-color:#b94a48;}
-.label-important[href],.badge-important[href]{background-color:#953b39;}
-.label-warning,.badge-warning{background-color:#f89406;}
-.label-warning[href],.badge-warning[href]{background-color:#c67605;}
-.label-success,.badge-success{background-color:#468847;}
-.label-success[href],.badge-success[href]{background-color:#356635;}
-.label-info,.badge-info{background-color:#3a87ad;}
-.label-info[href],.badge-info[href]{background-color:#2d6987;}
-.label-inverse,.badge-inverse{background-color:#333333;}
-.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a;}
-.btn .label,.btn .badge{position:relative;top:-1px;}
-.btn-mini .label,.btn-mini .badge{top:0;}
-table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0;}
-.table{width:100%;margin-bottom:20px;}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
-.table th{font-weight:bold;}
-.table thead th{vertical-align:bottom;}
-.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
-.table tbody+tbody{border-top:2px solid #dddddd;}
-.table-condensed th,.table-condensed td{padding:4px 5px;}
-.table-bordered{border:1px solid #dddddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
-.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
-.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;}
-.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px;}
-.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;}
-.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child,.table-bordered tfoot:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;}
-.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;}
-.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topleft:4px;}
-.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
-.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5;}
-table [class*=span],.row-fluid table [class*=span]{display:table-cell;float:none;margin-left:0;}
-.table .span1{float:none;width:44px;margin-left:0;}
-.table .span2{float:none;width:124px;margin-left:0;}
-.table .span3{float:none;width:204px;margin-left:0;}
-.table .span4{float:none;width:284px;margin-left:0;}
-.table .span5{float:none;width:364px;margin-left:0;}
-.table .span6{float:none;width:444px;margin-left:0;}
-.table .span7{float:none;width:524px;margin-left:0;}
-.table .span8{float:none;width:604px;margin-left:0;}
-.table .span9{float:none;width:684px;margin-left:0;}
-.table .span10{float:none;width:764px;margin-left:0;}
-.table .span11{float:none;width:844px;margin-left:0;}
-.table .span12{float:none;width:924px;margin-left:0;}
-.table .span13{float:none;width:1004px;margin-left:0;}
-.table .span14{float:none;width:1084px;margin-left:0;}
-.table .span15{float:none;width:1164px;margin-left:0;}
-.table .span16{float:none;width:1244px;margin-left:0;}
-.table .span17{float:none;width:1324px;margin-left:0;}
-.table .span18{float:none;width:1404px;margin-left:0;}
-.table .span19{float:none;width:1484px;margin-left:0;}
-.table .span20{float:none;width:1564px;margin-left:0;}
-.table .span21{float:none;width:1644px;margin-left:0;}
-.table .span22{float:none;width:1724px;margin-left:0;}
-.table .span23{float:none;width:1804px;margin-left:0;}
-.table .span24{float:none;width:1884px;margin-left:0;}
-.table tbody tr.success td{background-color:#dff0d8;}
-.table tbody tr.error td{background-color:#f2dede;}
-.table tbody tr.warning td{background-color:#fcf8e3;}
-.table tbody tr.info td{background-color:#d9edf7;}
-.table-hover tbody tr.success:hover td{background-color:#d0e9c6;}
-.table-hover tbody tr.error:hover td{background-color:#ebcccc;}
-.table-hover tbody tr.warning:hover td{background-color:#faf2cc;}
-.table-hover tbody tr.info:hover td{background-color:#c4e3f3;}
-form{margin:0 0 20px;}
-fieldset{padding:0;margin:0;border:0;}
-legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333333;border:0;border-bottom:1px solid #e5e5e5;}legend small{font-size:15px;color:#999999;}
-label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px;}
-input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
-label{display:block;margin-bottom:5px;}
-select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px;font-size:14px;line-height:20px;color:#555555;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-input,textarea,.uneditable-input{width:206px;}
-textarea{height:auto;}
-textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#ffffff;border:1px solid #cccccc;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear .2s, box-shadow linear .2s;-moz-transition:border linear .2s, box-shadow linear .2s;-o-transition:border linear .2s, box-shadow linear .2s;transition:border linear .2s, box-shadow linear .2s;}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82, 168, 236, 0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);}
-input[type="radio"],input[type="checkbox"]{margin:4px 0 0;*margin-top:0;margin-top:1px \9;line-height:normal;cursor:pointer;}
-input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto;}
-select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px;}
-select{width:220px;border:1px solid #cccccc;background-color:#ffffff;}
-select[multiple],select[size]{height:auto;}
-select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-.uneditable-input,.uneditable-textarea{color:#999999;background-color:#fcfcfc;border-color:#cccccc;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
-.uneditable-input{overflow:hidden;white-space:nowrap;}
-.uneditable-textarea{width:auto;height:auto;}
-input:-moz-placeholder,textarea:-moz-placeholder{color:#999999;}
-input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999999;}
-input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999999;}
-.radio,.checkbox{min-height:18px;padding-left:18px;}
-.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
-.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
-.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
-.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
-.input-mini{width:60px;}
-.input-small{width:90px;}
-.input-medium{width:150px;}
-.input-large{width:210px;}
-.input-xlarge{width:270px;}
-.input-xxlarge{width:530px;}
-input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0;}
-.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block;}
-input,textarea,.uneditable-input{margin-left:0;}
-.controls-row [class*="span"]+[class*="span"]{margin-left:20px;}
-input.span12, textarea.span12, .uneditable-input.span12{width:926px;}
-input.span11, textarea.span11, .uneditable-input.span11{width:846px;}
-input.span10, textarea.span10, .uneditable-input.span10{width:766px;}
-input.span9, textarea.span9, .uneditable-input.span9{width:686px;}
-input.span8, textarea.span8, .uneditable-input.span8{width:606px;}
-input.span7, textarea.span7, .uneditable-input.span7{width:526px;}
-input.span6, textarea.span6, .uneditable-input.span6{width:446px;}
-input.span5, textarea.span5, .uneditable-input.span5{width:366px;}
-input.span4, textarea.span4, .uneditable-input.span4{width:286px;}
-input.span3, textarea.span3, .uneditable-input.span3{width:206px;}
-input.span2, textarea.span2, .uneditable-input.span2{width:126px;}
-input.span1, textarea.span1, .uneditable-input.span1{width:46px;}
-.controls-row{*zoom:1;}.controls-row:before,.controls-row:after{display:table;content:"";line-height:0;}
-.controls-row:after{clear:both;}
-.controls-row [class*="span"]{float:left;}
-input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eeeeee;}
-input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent;}
-.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
-.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;}
-.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;}
-.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
-.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
-.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;}
-.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;}
-.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
-.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
-.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;}
-.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;}
-.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
-.control-group.info>label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad;}
-.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad;}
-.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;}
-.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad;}
-input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
-.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";line-height:0;}
-.form-actions:after{clear:both;}
-.help-block,.help-inline{color:#595959;}
-.help-block{display:block;margin-bottom:10px;}
-.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
-.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap;}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;font-size:14px;vertical-align:top;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2;}
-.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#eeeeee;border:1px solid #ccc;}
-.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546;}
-.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
-.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-append .add-on,.input-append .btn{margin-left:-1px;}
-.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;}
-.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;}
-.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;}
-.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;}
-.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;*zoom:1;margin-bottom:0;vertical-align:middle;}
-.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
-.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block;}
-.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
-.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
-.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0;}
-.control-group{margin-bottom:10px;}
-legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate;}
-.form-horizontal .control-group{margin-bottom:20px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";line-height:0;}
-.form-horizontal .control-group:after{clear:both;}
-.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right;}
-.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0;}.form-horizontal .controls:first-child{*padding-left:180px;}
-.form-horizontal .help-block{margin-bottom:0;}
-.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block{margin-top:10px;}
-.form-horizontal .form-actions{padding-left:180px;}
-.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 14px;margin-bottom:0;font-size:14px;line-height:20px;*line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333333;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(to bottom, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #bbbbbb;*border:0;border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333333;background-color:#e6e6e6;*background-color:#d9d9d9;}
-.btn:active,.btn.active{background-color:#cccccc \9;}
-.btn:first-child{*margin-left:0;}
-.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
-.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);}
-.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
-.btn-large{padding:9px 14px;font-size:16px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
-.btn-large [class^="icon-"]{margin-top:2px;}
-.btn-small{padding:3px 9px;font-size:12px;line-height:18px;}
-.btn-small [class^="icon-"]{margin-top:0;}
-.btn-mini{padding:2px 6px;font-size:11px;line-height:17px;}
-.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}
-.btn-block+.btn-block{margin-top:5px;}
-input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%;}
-.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
-.btn{border-color:#c5c5c5;border-color:rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25);}
-.btn-primary{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(to bottom, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#0044cc;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#ffffff;background-color:#0044cc;*background-color:#003bb3;}
-.btn-primary:active,.btn-primary.active{background-color:#003399 \9;}
-.btn-warning{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#ffffff;background-color:#f89406;*background-color:#df8505;}
-.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
-.btn-danger{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(to bottom, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#ffffff;background-color:#bd362f;*background-color:#a9302a;}
-.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
-.btn-success{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(to bottom, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#ffffff;background-color:#51a351;*background-color:#499249;}
-.btn-success:active,.btn-success.active{background-color:#408140 \9;}
-.btn-info{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(to bottom, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#ffffff;background-color:#2f96b4;*background-color:#2a85a0;}
-.btn-info:active,.btn-info.active{background-color:#24748c \9;}
-.btn-inverse{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#363636;background-image:-moz-linear-gradient(top, #444444, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222));background-image:-webkit-linear-gradient(top, #444444, #222222);background-image:-o-linear-gradient(top, #444444, #222222);background-image:linear-gradient(to bottom, #444444, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#222222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#ffffff;background-color:#222222;*background-color:#151515;}
-.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
-button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
-button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
-button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
-button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
-.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
-.btn-link{border-color:transparent;cursor:pointer;color:#0088cc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent;}
-.btn-link[disabled]:hover{color:#333333;text-decoration:none;}
-[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px;}
-.icon-white,.nav-tabs>.active>a>[class^="icon-"],.nav-tabs>.active>a>[class*=" icon-"],.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png");}
-.icon-glass{background-position:0 0;}
-.icon-music{background-position:-24px 0;}
-.icon-search{background-position:-48px 0;}
-.icon-envelope{background-position:-72px 0;}
-.icon-heart{background-position:-96px 0;}
-.icon-star{background-position:-120px 0;}
-.icon-star-empty{background-position:-144px 0;}
-.icon-user{background-position:-168px 0;}
-.icon-film{background-position:-192px 0;}
-.icon-th-large{background-position:-216px 0;}
-.icon-th{background-position:-240px 0;}
-.icon-th-list{background-position:-264px 0;}
-.icon-ok{background-position:-288px 0;}
-.icon-remove{background-position:-312px 0;}
-.icon-zoom-in{background-position:-336px 0;}
-.icon-zoom-out{background-position:-360px 0;}
-.icon-off{background-position:-384px 0;}
-.icon-signal{background-position:-408px 0;}
-.icon-cog{background-position:-432px 0;}
-.icon-trash{background-position:-456px 0;}
-.icon-home{background-position:0 -24px;}
-.icon-file{background-position:-24px -24px;}
-.icon-time{background-position:-48px -24px;}
-.icon-road{background-position:-72px -24px;}
-.icon-download-alt{background-position:-96px -24px;}
-.icon-download{background-position:-120px -24px;}
-.icon-upload{background-position:-144px -24px;}
-.icon-inbox{background-position:-168px -24px;}
-.icon-play-circle{background-position:-192px -24px;}
-.icon-repeat{background-position:-216px -24px;}
-.icon-refresh{background-position:-240px -24px;}
-.icon-list-alt{background-position:-264px -24px;}
-.icon-lock{background-position:-287px -24px;}
-.icon-flag{background-position:-312px -24px;}
-.icon-headphones{background-position:-336px -24px;}
-.icon-volume-off{background-position:-360px -24px;}
-.icon-volume-down{background-position:-384px -24px;}
-.icon-volume-up{background-position:-408px -24px;}
-.icon-qrcode{background-position:-432px -24px;}
-.icon-barcode{background-position:-456px -24px;}
-.icon-tag{background-position:0 -48px;}
-.icon-tags{background-position:-25px -48px;}
-.icon-book{background-position:-48px -48px;}
-.icon-bookmark{background-position:-72px -48px;}
-.icon-print{background-position:-96px -48px;}
-.icon-camera{background-position:-120px -48px;}
-.icon-font{background-position:-144px -48px;}
-.icon-bold{background-position:-167px -48px;}
-.icon-italic{background-position:-192px -48px;}
-.icon-text-height{background-position:-216px -48px;}
-.icon-text-width{background-position:-240px -48px;}
-.icon-align-left{background-position:-264px -48px;}
-.icon-align-center{background-position:-288px -48px;}
-.icon-align-right{background-position:-312px -48px;}
-.icon-align-justify{background-position:-336px -48px;}
-.icon-list{background-position:-360px -48px;}
-.icon-indent-left{background-position:-384px -48px;}
-.icon-indent-right{background-position:-408px -48px;}
-.icon-facetime-video{background-position:-432px -48px;}
-.icon-picture{background-position:-456px -48px;}
-.icon-pencil{background-position:0 -72px;}
-.icon-map-marker{background-position:-24px -72px;}
-.icon-adjust{background-position:-48px -72px;}
-.icon-tint{background-position:-72px -72px;}
-.icon-edit{background-position:-96px -72px;}
-.icon-share{background-position:-120px -72px;}
-.icon-check{background-position:-144px -72px;}
-.icon-move{background-position:-168px -72px;}
-.icon-step-backward{background-position:-192px -72px;}
-.icon-fast-backward{background-position:-216px -72px;}
-.icon-backward{background-position:-240px -72px;}
-.icon-play{background-position:-264px -72px;}
-.icon-pause{background-position:-288px -72px;}
-.icon-stop{background-position:-312px -72px;}
-.icon-forward{background-position:-336px -72px;}
-.icon-fast-forward{background-position:-360px -72px;}
-.icon-step-forward{background-position:-384px -72px;}
-.icon-eject{background-position:-408px -72px;}
-.icon-chevron-left{background-position:-432px -72px;}
-.icon-chevron-right{background-position:-456px -72px;}
-.icon-plus-sign{background-position:0 -96px;}
-.icon-minus-sign{background-position:-24px -96px;}
-.icon-remove-sign{background-position:-48px -96px;}
-.icon-ok-sign{background-position:-72px -96px;}
-.icon-question-sign{background-position:-96px -96px;}
-.icon-info-sign{background-position:-120px -96px;}
-.icon-screenshot{background-position:-144px -96px;}
-.icon-remove-circle{background-position:-168px -96px;}
-.icon-ok-circle{background-position:-192px -96px;}
-.icon-ban-circle{background-position:-216px -96px;}
-.icon-arrow-left{background-position:-240px -96px;}
-.icon-arrow-right{background-position:-264px -96px;}
-.icon-arrow-up{background-position:-289px -96px;}
-.icon-arrow-down{background-position:-312px -96px;}
-.icon-share-alt{background-position:-336px -96px;}
-.icon-resize-full{background-position:-360px -96px;}
-.icon-resize-small{background-position:-384px -96px;}
-.icon-plus{background-position:-408px -96px;}
-.icon-minus{background-position:-433px -96px;}
-.icon-asterisk{background-position:-456px -96px;}
-.icon-exclamation-sign{background-position:0 -120px;}
-.icon-gift{background-position:-24px -120px;}
-.icon-leaf{background-position:-48px -120px;}
-.icon-fire{background-position:-72px -120px;}
-.icon-eye-open{background-position:-96px -120px;}
-.icon-eye-close{background-position:-120px -120px;}
-.icon-warning-sign{background-position:-144px -120px;}
-.icon-plane{background-position:-168px -120px;}
-.icon-calendar{background-position:-192px -120px;}
-.icon-random{background-position:-216px -120px;width:16px;}
-.icon-comment{background-position:-240px -120px;}
-.icon-magnet{background-position:-264px -120px;}
-.icon-chevron-up{background-position:-288px -120px;}
-.icon-chevron-down{background-position:-313px -119px;}
-.icon-retweet{background-position:-336px -120px;}
-.icon-shopping-cart{background-position:-360px -120px;}
-.icon-folder-close{background-position:-384px -120px;}
-.icon-folder-open{background-position:-408px -120px;width:16px;}
-.icon-resize-vertical{background-position:-432px -119px;}
-.icon-resize-horizontal{background-position:-456px -118px;}
-.icon-hdd{background-position:0 -144px;}
-.icon-bullhorn{background-position:-24px -144px;}
-.icon-bell{background-position:-48px -144px;}
-.icon-certificate{background-position:-72px -144px;}
-.icon-thumbs-up{background-position:-96px -144px;}
-.icon-thumbs-down{background-position:-120px -144px;}
-.icon-hand-right{background-position:-144px -144px;}
-.icon-hand-left{background-position:-168px -144px;}
-.icon-hand-up{background-position:-192px -144px;}
-.icon-hand-down{background-position:-216px -144px;}
-.icon-circle-arrow-right{background-position:-240px -144px;}
-.icon-circle-arrow-left{background-position:-264px -144px;}
-.icon-circle-arrow-up{background-position:-288px -144px;}
-.icon-circle-arrow-down{background-position:-312px -144px;}
-.icon-globe{background-position:-336px -144px;}
-.icon-wrench{background-position:-360px -144px;}
-.icon-tasks{background-position:-384px -144px;}
-.icon-filter{background-position:-408px -144px;}
-.icon-briefcase{background-position:-432px -144px;}
-.icon-fullscreen{background-position:-456px -144px;}
-.btn-group{position:relative;font-size:0;vertical-align:middle;white-space:nowrap;*margin-left:.3em;}.btn-group:first-child{*margin-left:0;}
-.btn-group+.btn-group{margin-left:5px;}
-.btn-toolbar{font-size:0;margin-top:10px;margin-bottom:10px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
-.btn-toolbar .btn+.btn,.btn-toolbar .btn-group+.btn,.btn-toolbar .btn+.btn-group{margin-left:5px;}
-.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-group>.btn+.btn{margin-left:-1px;}
-.btn-group>.btn,.btn-group>.dropdown-menu{font-size:14px;}
-.btn-group>.btn-mini{font-size:11px;}
-.btn-group>.btn-small{font-size:12px;}
-.btn-group>.btn-large{font-size:16px;}
-.btn-group>.btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
-.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
-.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
-.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
-.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2;}
-.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
-.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;}
-.btn-group>.btn-mini+.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:2px;*padding-bottom:2px;}
-.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px;}
-.btn-group>.btn-large+.dropdown-toggle{padding-left:12px;padding-right:12px;*padding-top:7px;*padding-bottom:7px;}
-.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);}
-.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6;}
-.btn-group.open .btn-primary.dropdown-toggle{background-color:#0044cc;}
-.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406;}
-.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f;}
-.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351;}
-.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4;}
-.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222222;}
-.btn .caret{margin-top:8px;margin-left:0;}
-.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px;}
-.btn-large .caret{border-left-width:5px;border-right-width:5px;border-top-width:5px;}
-.dropup .btn-large .caret{border-bottom:5px solid #000000;border-top:0;}
-.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
-.btn-group-vertical{display:inline-block;*display:inline;*zoom:1;}
-.btn-group-vertical .btn{display:block;float:none;width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-group-vertical .btn+.btn{margin-left:0;margin-top:-1px;}
-.btn-group-vertical .btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
-.btn-group-vertical .btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
-.btn-group-vertical .btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0;}
-.btn-group-vertical .btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
-.nav{margin-left:0;margin-bottom:20px;list-style:none;}
-.nav>li>a{display:block;}
-.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
-.nav>.pull-right{float:right;}
-.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
-.nav li+.nav-header{margin-top:9px;}
-.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
-.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
-.nav-list>li>a{padding:3px 15px;}
-.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
-.nav-list [class^="icon-"]{margin-right:2px;}
-.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;}
-.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";line-height:0;}
-.nav-tabs:after,.nav-pills:after{clear:both;}
-.nav-tabs>li,.nav-pills>li{float:left;}
-.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
-.nav-tabs{border-bottom:1px solid #ddd;}
-.nav-tabs>li{margin-bottom:-1px;}
-.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
-.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
-.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
-.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;}
-.nav-stacked>li{float:none;}
-.nav-stacked>li>a{margin-right:0;}
-.nav-tabs.nav-stacked{border-bottom:0;}
-.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;}
-.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
-.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
-.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
-.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
-.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
-.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.nav .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;}
-.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;}
-.nav-tabs .dropdown-toggle .caret{margin-top:8px;}
-.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff;}
-.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;}
-.nav>.dropdown.active>a:hover{cursor:pointer;}
-.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
-.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
-.tabs-stacked .open>a:hover{border-color:#999999;}
-.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";line-height:0;}
-.tabbable:after{clear:both;}
-.tab-content{overflow:auto;}
-.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0;}
-.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
-.tab-content>.active,.pill-content>.active{display:block;}
-.tabs-below>.nav-tabs{border-top:1px solid #ddd;}
-.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0;}
-.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below>.nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
-.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd;}
-.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none;}
-.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
-.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
-.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
-.tabs-left>.nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
-.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
-.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
-.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
-.tabs-right>.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
-.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
-.nav>.disabled>a{color:#999999;}
-.nav>.disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;}
-.navbar{overflow:visible;margin-bottom:20px;color:#777777;*position:relative;*z-index:2;}
-.navbar-inner{min-height:40px;padding-left:20px;padding-right:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top, #ffffff, #f2f2f2);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));background-image:-webkit-linear-gradient(top, #ffffff, #f2f2f2);background-image:-o-linear-gradient(top, #ffffff, #f2f2f2);background-image:linear-gradient(to bottom, #ffffff, #f2f2f2);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);-moz-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);*zoom:1;}.navbar-inner:before,.navbar-inner:after{display:table;content:"";line-height:0;}
-.navbar-inner:after{clear:both;}
-.navbar .container{width:auto;}
-.nav-collapse.collapse{height:auto;}
-.navbar .brand{float:left;display:block;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777777;text-shadow:0 1px 0 #ffffff;}.navbar .brand:hover{text-decoration:none;}
-.navbar-text{margin-bottom:0;line-height:40px;}
-.navbar-link{color:#777777;}.navbar-link:hover{color:#333333;}
-.navbar .divider-vertical{height:40px;margin:0 9px;border-left:1px solid #f2f2f2;border-right:1px solid #ffffff;}
-.navbar .btn,.navbar .btn-group{margin-top:5px;}
-.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0;}
-.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";line-height:0;}
-.navbar-form:after{clear:both;}
-.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
-.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0;}
-.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
-.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
-.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0;}.navbar-search .search-query{margin-bottom:0;padding:4px 14px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.navbar-static-top{position:static;width:100%;margin-bottom:0;}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
-.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px;}
-.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0;}
-.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
-.navbar-fixed-top{top:0;}
-.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);}
-.navbar-fixed-bottom{bottom:0;}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);}
-.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
-.navbar .nav.pull-right{float:right;margin-right:0;}
-.navbar .nav>li{float:left;}
-.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777777;text-decoration:none;text-shadow:0 1px 0 #ffffff;}
-.navbar .nav .dropdown-toggle .caret{margin-top:8px;}
-.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{background-color:transparent;color:#333333;text-decoration:none;}
-.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);-moz-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);}
-.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#ededed;background-image:-moz-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5));background-image:-webkit-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-o-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:linear-gradient(to bottom, #f2f2f2, #e5e5e5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0);border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e5e5e5;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#ffffff;background-color:#e5e5e5;*background-color:#d9d9d9;}
-.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#cccccc \9;}
-.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
-.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
-.navbar .nav>li>.dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
-.navbar .nav>li>.dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
-.navbar-fixed-bottom .nav>li>.dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
-.navbar-fixed-bottom .nav>li>.dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
-.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e5e5e5;color:#555555;}
-.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777777;border-bottom-color:#777777;}
-.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;}
-.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{left:auto;right:0;}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{left:auto;right:12px;}
-.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{left:auto;right:13px;}
-.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{left:auto;right:100%;margin-left:0;margin-right:-1px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px;}
-.navbar-inverse{color:#999999;}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top, #222222, #111111);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));background-image:-webkit-linear-gradient(top, #222222, #111111);background-image:-o-linear-gradient(top, #222222, #111111);background-image:linear-gradient(to bottom, #222222, #111111);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);border-color:#252525;}
-.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999999;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#ffffff;}
-.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{background-color:transparent;color:#ffffff;}
-.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#ffffff;background-color:#111111;}
-.navbar-inverse .navbar-link{color:#999999;}.navbar-inverse .navbar-link:hover{color:#ffffff;}
-.navbar-inverse .divider-vertical{border-left-color:#111111;border-right-color:#222222;}
-.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{background-color:#111111;color:#ffffff;}
-.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999999;border-bottom-color:#999999;}
-.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
-.navbar-inverse .navbar-search .search-query{color:#ffffff;background-color:#515151;border-color:#111111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
-.navbar-inverse .btn-navbar{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e0e0e;background-image:-moz-linear-gradient(top, #151515, #040404);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));background-image:-webkit-linear-gradient(top, #151515, #040404);background-image:-o-linear-gradient(top, #151515, #040404);background-image:linear-gradient(to bottom, #151515, #040404);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0);border-color:#040404 #040404 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#040404;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#ffffff;background-color:#040404;*background-color:#000000;}
-.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000000 \9;}
-.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
-.breadcrumb .divider{padding:0 5px;color:#ccc;}
-.breadcrumb .active{color:#999999;}
-.pagination{height:40px;margin:20px 0;}
-.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
-.pagination ul>li{display:inline;}
-.pagination ul>li>a,.pagination ul>li>span{float:left;padding:0 14px;line-height:38px;text-decoration:none;background-color:#ffffff;border:1px solid #dddddd;border-left-width:0;}
-.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5;}
-.pagination ul>.active>a,.pagination ul>.active>span{color:#999999;cursor:default;}
-.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999999;background-color:transparent;cursor:default;}
-.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-.pagination-centered{text-align:center;}
-.pagination-right{text-align:right;}
-.pager{margin:20px 0;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";line-height:0;}
-.pager:after{clear:both;}
-.pager li{display:inline;}
-.pager a,.pager span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
-.pager .next a,.pager .next span{float:right;}
-.pager .previous a{float:left;}
-.pager .disabled a,.pager .disabled a:hover,.pager .disabled span{color:#999999;background-color:#fff;cursor:default;}
-.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";line-height:0;}
-.thumbnails:after{clear:both;}
-.row-fluid .thumbnails{margin-left:0;}
-.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px;}
-.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;-o-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}
-a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
-.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
-.thumbnail .caption{padding:9px;color:#555555;}
-.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
-.alert h4{margin:0;}
-.alert .close{position:relative;top:-2px;right:-21px;line-height:20px;}
-.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
-.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
-.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
-.alert-block{padding-top:14px;padding-bottom:14px;}
-.alert-block>p,.alert-block>ul{margin-bottom:0;}
-.alert-block p+p{margin-top:5px;}
-@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.progress .bar{width:0%;height:100%;color:#ffffff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
-.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);}
-.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
-.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
-.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(to bottom, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);}
-.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(to bottom, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);}
-.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(to bottom, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);}
-.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);}
-.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
-.hero-unit p{font-size:18px;font-weight:200;line-height:30px;color:inherit;}
-.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
-.tooltip.top{margin-top:-3px;}
-.tooltip.right{margin-left:3px;}
-.tooltip.bottom{margin-top:3px;}
-.tooltip.left{margin-left:-3px;}
-.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid;}
-.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000;}
-.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000;}
-.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000;}
-.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000;}
-.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;background-color:#ffffff;-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);}.popover.top{margin-bottom:10px;}
-.popover.right{margin-left:10px;}
-.popover.bottom{margin-top:10px;}
-.popover.left{margin-right:10px;}
-.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;}
-.popover-content{padding:9px 14px;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
-.popover .arrow,.popover .arrow:after{position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid;}
-.popover .arrow:after{content:"";z-index:-1;}
-.popover.top .arrow{bottom:-10px;left:50%;margin-left:-10px;border-width:10px 10px 0;border-top-color:#ffffff;}.popover.top .arrow:after{border-width:11px 11px 0;border-top-color:rgba(0, 0, 0, 0.25);bottom:-1px;left:-11px;}
-.popover.right .arrow{top:50%;left:-10px;margin-top:-10px;border-width:10px 10px 10px 0;border-right-color:#ffffff;}.popover.right .arrow:after{border-width:11px 11px 11px 0;border-right-color:rgba(0, 0, 0, 0.25);bottom:-11px;left:-1px;}
-.popover.bottom .arrow{top:-10px;left:50%;margin-left:-10px;border-width:0 10px 10px;border-bottom-color:#ffffff;}.popover.bottom .arrow:after{border-width:0 11px 11px;border-bottom-color:rgba(0, 0, 0, 0.25);top:-1px;left:-11px;}
-.popover.left .arrow{top:50%;right:-10px;margin-top:-10px;border-width:10px 0 10px 10px;border-left-color:#ffffff;}.popover.left .arrow:after{border-width:11px 0 11px 11px;border-left-color:rgba(0, 0, 0, 0.25);bottom:-11px;right:-1px;}
-.modal-open .modal .dropdown-menu{z-index:2050;}
-.modal-open .modal .dropdown.open{*z-index:2050;}
-.modal-open .modal .popover{z-index:2060;}
-.modal-open .modal .tooltip{z-index:2080;}
-.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
-.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
-.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
-.modal.fade.in{top:50%;}
-.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
-.modal-header h3{margin:0;line-height:30px;}
-.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
-.modal-form{margin-bottom:0;}
-.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";line-height:0;}
-.modal-footer:after{clear:both;}
-.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
-.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
-.dropup,.dropdown{position:relative;}
-.dropdown-toggle{*margin-bottom:-3px;}
-.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
-.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:"";}
-.dropdown .caret{margin-top:8px;margin-left:2px;}
-.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#ffffff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;}.dropdown-menu.pull-right{right:0;left:auto;}
-.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;}
-.dropdown-menu a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333333;white-space:nowrap;}
-.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{text-decoration:none;color:#ffffff;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);}
-.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;outline:0;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);}
-.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999999;}
-.dropdown-menu .disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;}
-.open{*z-index:1000;}.open >.dropdown-menu{display:block;}
-.pull-right>.dropdown-menu{right:0;left:auto;}
-.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"";}
-.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
-.dropdown-submenu{position:relative;}
-.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;}
-.dropdown-submenu:hover>.dropdown-menu{display:block;}
-.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#cccccc;margin-top:5px;margin-right:-10px;}
-.dropdown-submenu:hover>a:after{border-left-color:#ffffff;}
-.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px;}
-.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.accordion{margin-bottom:20px;}
-.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.accordion-heading{border-bottom:0;}
-.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
-.accordion-toggle{cursor:pointer;}
-.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
-.carousel{position:relative;margin-bottom:20px;line-height:1;}
-.carousel-inner{overflow:hidden;width:100%;position:relative;}
-.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
-.carousel .item>img{display:block;line-height:1;}
-.carousel .active,.carousel .next,.carousel .prev{display:block;}
-.carousel .active{left:0;}
-.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
-.carousel .next{left:100%;}
-.carousel .prev{left:-100%;}
-.carousel .next.left,.carousel .prev.right{left:0;}
-.carousel .active.left{left:-100%;}
-.carousel .active.right{left:100%;}
-.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
-.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
-.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:15px;background:#333333;background:rgba(0, 0, 0, 0.75);}
-.carousel-caption h4,.carousel-caption p{color:#ffffff;line-height:20px;}
-.carousel-caption h4{margin:0 0 5px;}
-.carousel-caption p{margin-bottom:0;}
-.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
-.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;filter:alpha(opacity=40);}
-button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;}
-.pull-right{float:right;}
-.pull-left{float:left;}
-.hide{display:none;}
-.show{display:block;}
-.invisible{visibility:hidden;}
-.affix{position:fixed;}
-.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;}.fade.in{opacity:1;}
-.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;}.collapse.in{height:auto;}
-.hidden{display:none;visibility:hidden;}
-.visible-phone{display:none !important;}
-.visible-tablet{display:none !important;}
-.hidden-desktop{display:none !important;}
-.visible-desktop{display:inherit !important;}
-@media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade.in{top:auto;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}
+ * Bootstrap v3.2.0 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;width:100% \9;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px;line-height:1.42857143 \0}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;filter:alpha(opacity=0);opacity:0}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#428bca;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{min-width:30px;color:#777;background-color:transparent;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#428bca}.panel-primary>.panel-heading .badge{color:#428bca;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate3d(0,-25%,0);-o-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0)}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-size:12px;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}}
\ No newline at end of file
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index 0261a3038..4f52cc566 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -3,20 +3,20 @@
content running up underneath it. */
h1 {
- font-weight: 500;
+ font-weight: 500;
}
h2, h3 {
- font-weight: 300;
+ font-weight: 300;
}
.resource-description, .response-info {
- margin-bottom: 2em;
+ margin-bottom: 2em;
}
.version:before {
- content: "v";
- opacity: 0.6;
- padding-right: 0.25em;
+ content: "v";
+ opacity: 0.6;
+ padding-right: 0.25em;
}
.version {
@@ -24,16 +24,20 @@ h2, h3 {
}
.format-option {
- font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
+ font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
}
.button-form {
- float: right;
- margin-right: 1em;
+ float: right;
+ margin-right: 1em;
}
ul.breadcrumb {
- margin: 58px 0 0 0;
+ margin: 70px 0 0 0;
+}
+
+.breadcrumb li.active a {
+ color: #777;
}
form select, form input, form textarea {
@@ -43,17 +47,18 @@ form select, form input, form textarea {
form select[multiple] {
height: 150px;
}
+
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
}
.errorlist {
- margin-top: 0.5em;
+ margin-top: 0.5em;
}
pre {
@@ -64,8 +69,6 @@ pre {
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
}
-
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 000000000..4a4ca865d
Binary files /dev/null and b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot differ
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg
new file mode 100644
index 000000000..25691af8f
--- /dev/null
+++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg
@@ -0,0 +1,229 @@
+
+
+
\ No newline at end of file
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 000000000..67fa00bf8
Binary files /dev/null and b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf differ
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 000000000..8c54182aa
Binary files /dev/null and b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff differ
diff --git a/rest_framework/static/rest_framework/js/bootstrap.min.js b/rest_framework/static/rest_framework/js/bootstrap.min.js
index e0b220f40..7c1561a8b 100644
--- a/rest_framework/static/rest_framework/js/bootstrap.min.js
+++ b/rest_framework/static/rest_framework/js/bootstrap.min.js
@@ -1,7 +1,6 @@
-/**
-* Bootstrap.js by @fat & @mdo
-* plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js
-* Copyright 2012 Twitter, Inc.
-* http://www.apache.org/licenses/LICENSE-2.0.txt
-*/
-!function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;a("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1).focus(),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,a("body").removeClass("modal-open"),this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(a){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,a.proxy(this.removeBackdrop,this)):this.removeBackdrop()):b&&b()}},a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a(function(){a("body").on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})})}(window.jQuery),!function(a){function d(){e(a(b)).removeClass("open")}function e(b){var c=b.attr("data-target"),d;return c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,"")),d=a(c),d.length||(d=b.parent()),d}var b="[data-toggle=dropdown]",c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),f,g;if(c.is(".disabled, :disabled"))return;return f=e(c),g=f.hasClass("open"),d(),g||(f.toggleClass("open"),c.focus()),!1},keydown:function(b){var c,d,f,g,h,i;if(!/(38|40|27)/.test(b.keyCode))return;c=a(this),b.preventDefault(),b.stopPropagation();if(c.is(".disabled, :disabled"))return;g=e(c),h=g.hasClass("open");if(!h||h&&b.keyCode==27)return c.click();d=a("[role=menu] li:not(.divider) a",g);if(!d.length)return;i=d.index(d.filter(":focus")),b.keyCode==38&&i>0&&i--,b.keyCode==40&&i a",this.$body=a("body"),this.refresh(),this.process()}b.prototype={constructor:b,refresh:function(){var b=this,c;this.offsets=a([]),this.targets=a([]),c=this.$body.find(this.selector).map(function(){var b=a(this),c=b.data("target")||b.attr("href"),d=/^#\w/.test(c)&&a(c);return d&&d.length&&[[d.position().top,c]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},process:function(){var a=this.$scrollElement.scrollTop()+this.options.offset,b=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,c=b-this.$scrollElement.height(),d=this.offsets,e=this.targets,f=this.activeTarget,g;if(a>=c)return f!=(g=e.last()[0])&&this.activate(g);for(g=d.length;g--;)f!=e[g]&&a>=d[g]&&(!d[g+1]||a<=d[g+1])&&this.activate(e[g])},activate:function(b){var c,d;this.activeTarget=b,a(this.selector).parent(".active").removeClass("active"),d=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',c=a(d).parent("li").addClass("active"),c.parent(".dropdown-menu").length&&(c=c.closest("li.dropdown").addClass("active")),c.trigger("activate")}},a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f=typeof c=="object"&&c;e||d.data("scrollspy",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active a").last()[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}},a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a(function(){a("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})})}(window.jQuery),!function(a){var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f;this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this)):this.options.trigger!="manual"&&(e=this.options.trigger=="hover"?"mouseenter":"focus",f=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(e+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f+"."+this.type,this.options.selector,a.proxy(this.leave,this))),this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,b,this.$element.data()),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);if(!c.options.delay||!c.options.delay.show)return c.show();clearTimeout(this.timeout),c.hoverState="in",this.timeout=setTimeout(function(){c.hoverState=="in"&&c.show()},c.options.delay.show)},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!c.options.delay||!c.options.delay.hide)return c.hide();c.hoverState="out",this.timeout=setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide)},show:function(){var a,b,c,d,e,f,g;if(this.hasContent()&&this.enabled){a=this.tip(),this.setContent(),this.options.animation&&a.addClass("fade"),f=typeof this.options.placement=="function"?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,b=/in/.test(f),a.remove().css({top:0,left:0,display:"block"}).appendTo(b?this.$element:document.body),c=this.getPosition(b),d=a[0].offsetWidth,e=a[0].offsetHeight;switch(b?f.split(" ")[1]:f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}a.css(g).addClass(f).addClass("in")}},setContent:function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},hide:function(){function d(){var b=setTimeout(function(){c.off(a.support.transition.end).remove()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.remove()})}var b=this,c=this.tip();return c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d():c.remove(),this},fixTitle:function(){var a=this.$element;(a.attr("title")||typeof a.attr("data-original-title")!="string")&&a.attr("data-original-title",a.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(b){return a.extend({},b?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||(typeof c.title=="function"?c.title.call(b[0]):c.title),a},tip:function(){return this.$tip=this.$tip||a(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f=typeof c=="object"&&c;e||d.data("tooltip",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(a){var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content > *")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-content")||(typeof c.content=="function"?c.content.call(b[0]):c.content),a},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f=typeof c=="object"&&c;e||d.data("popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:''})}(window.jQuery),!function(a){var b=function(b,c){this.options=a.extend({},a.fn.affix.defaults,c),this.$window=a(window).on("scroll.affix.data-api",a.proxy(this.checkPosition,this)),this.$element=a(b),this.checkPosition()};b.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var b=a(document).height(),c=this.$window.scrollTop(),d=this.$element.offset(),e=this.options.offset,f=e.bottom,g=e.top,h="affix affix-top affix-bottom",i;typeof e!="object"&&(f=g=e),typeof g=="function"&&(g=e.top()),typeof f=="function"&&(f=e.bottom()),i=this.unpin!=null&&c+this.unpin<=d.top?!1:f!=null&&d.top+this.$element.height()>=b-f?"bottom":g!=null&&c<=g?"top":!1;if(this.affixed===i)return;this.affixed=i,this.unpin=i=="bottom"?d.top-c:null,this.$element.removeClass(h).addClass("affix"+(i?"-"+i:""))},a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("affix"),f=typeof c=="object"&&c;e||d.data("affix",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.defaults={offset:0},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery),!function(a){var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),a.support.transition&&this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();c[a(e).hasClass("in")?"addClass":"removeClass"]("collapsed"),a(e).collapse(f)})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".item.active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide",{relatedTarget:e[0]});this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c),g=typeof c=="string"?c:f.slide;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):g?e[g]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=a(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(a)).change(),this.hide()},updater:function(a){return a},show:function(){var b=a.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:b.top+b.height,left:b.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(b){var c;return this.query=this.$element.val(),!this.query||this.query.length"+b+""})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),(a.browser.chrome||a.browser.webkit||a.browser.msie)&&this.$element.on("keydown",a.proxy(this.keydown,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this))},move:function(a){if(!this.shown)return;switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()},keydown:function(b){this.suppressKeyPressRepeat=!~a.inArray(b.keyCode,[40,38,9,13,27]),this.move(b)},keypress:function(a){if(this.suppressKeyPressRepeat)return;this.move(a)},keyup:function(a){switch(a.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},blur:function(a){var b=this;setTimeout(function(){b.hide()},150)},click:function(a){a.stopPropagation(),a.preventDefault(),this.select()},mouseenter:function(b){this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")}},a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f=typeof c=="object"&&c;e||d.data("typeahead",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:' ',minLength:1},a.fn.typeahead.Constructor=b,a(function(){a("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;b.preventDefault(),c.typeahead(c.data())})})}(window.jQuery)
\ No newline at end of file
+/*!
+ * Bootstrap v3.2.0 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.divider):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j ').appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;e?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(150):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var f=function(){c.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",f).emulateTransitionEnd(150):f()}else b&&b()},c.prototype.checkScrollbar=function(){document.body.clientWidth>=window.innerWidth||(this.scrollbarWidth=this.scrollbarWidth||this.measureScrollbar())},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.scrollbarWidth&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right","")},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};c.VERSION="3.2.0",c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show()},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var c=a.contains(document.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!c)return;var d=this,e=this.tip(),f=this.getUID(this.type);this.setContent(),e.attr("id",f),this.$element.attr("aria-describedby",f),this.options.animation&&e.addClass("fade");var g="function"==typeof this.options.placement?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,h=/\s?auto?\s?/i,i=h.test(g);i&&(g=g.replace(h,"")||"top"),e.detach().css({top:0,left:0,display:"block"}).addClass(g).data("bs."+this.type,this),this.options.container?e.appendTo(this.options.container):e.insertAfter(this.$element);var j=this.getPosition(),k=e[0].offsetWidth,l=e[0].offsetHeight;if(i){var m=g,n=this.$element.parent(),o=this.getPosition(n);g="bottom"==g&&j.top+j.height+l-o.scroll>o.height?"top":"top"==g&&j.top-o.scroll-l<0?"bottom":"right"==g&&j.right+k>o.width?"left":"left"==g&&j.left-kg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){clearTimeout(this.timeout),this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.2.0",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").empty()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},c.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){var e=a.proxy(this.process,this);this.$body=a("body"),this.$scrollElement=a(a(c).is("body")?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",e),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.2.0",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b="offset",c=0;a.isWindow(this.$scrollElement[0])||(b="position",c=this.$scrollElement.scrollTop()),this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight();var d=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+c,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){d.offsets.push(this[0]),d.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.2.0",c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.closest("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},c.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one("bsTransitionEnd",e).emulateTransitionEnd(150):e(),f.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(c){c.preventDefault(),b.call(a(this),"show")})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.2.0",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=a(document).height(),d=this.$target.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=b-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){null!=this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:b-this.$element.height()-h}))}}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},d.offsetBottom&&(d.offset.bottom=d.offsetBottom),d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery);
\ No newline at end of file
diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js
index c74829d7d..c8812132c 100644
--- a/rest_framework/static/rest_framework/js/default.js
+++ b/rest_framework/static/rest_framework/js/default.js
@@ -1,13 +1,57 @@
+function getCookie(c_name)
+{
+ // From http://www.w3schools.com/js/js_cookies.asp
+ var c_value = document.cookie;
+ var c_start = c_value.indexOf(" " + c_name + "=");
+ if (c_start == -1) {
+ c_start = c_value.indexOf(c_name + "=");
+ }
+ if (c_start == -1) {
+ c_value = null;
+ } else {
+ c_start = c_value.indexOf("=", c_start) + 1;
+ var c_end = c_value.indexOf(";", c_start);
+ if (c_end == -1) {
+ c_end = c_value.length;
+ }
+ c_value = unescape(c_value.substring(c_start,c_end));
+ }
+ return c_value;
+}
+
+// JSON highlighting.
prettyPrint();
+// Bootstrap tooltips.
$('.js-tooltip').tooltip({
- delay: 1000
+ delay: 1000,
+ container: 'body'
});
+// Deal with rounded tab styling after tab clicks.
$('a[data-toggle="tab"]:first').on('shown', function (e) {
$(e.target).parents('.tabbable').addClass('first-tab-active');
});
$('a[data-toggle="tab"]:not(:first)').on('shown', function (e) {
$(e.target).parents('.tabbable').removeClass('first-tab-active');
});
-$('.form-switcher a:first').tab('show');
+
+$('a[data-toggle="tab"]').click(function(){
+ document.cookie="tabstyle=" + this.name + "; path=/";
+});
+
+// Store tab preference in cookies & display appropriate tab on load.
+var selectedTab = null;
+var selectedTabName = getCookie('tabstyle');
+
+if (selectedTabName) {
+ selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
+}
+
+if (selectedTab && selectedTab.length > 0) {
+ // Display whichever tab is selected.
+ selectedTab.tab('show');
+} else {
+ // If no tab selected, display rightmost tab.
+ $('.form-switcher a:first').tab('show');
+}
diff --git a/rest_framework/status.py b/rest_framework/status.py
index b9f249f9f..90a755089 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -6,6 +6,27 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
from __future__ import unicode_literals
+
+def is_informational(code):
+ return code >= 100 and code <= 199
+
+
+def is_success(code):
+ return code >= 200 and code <= 299
+
+
+def is_redirect(code):
+ return code >= 300 and code <= 399
+
+
+def is_client_error(code):
+ return code >= 400 and code <= 499
+
+
+def is_server_error(code):
+ return code >= 500 and code <= 599
+
+
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
diff --git a/rest_framework/templates/rest_framework/api_form.html b/rest_framework/templates/rest_framework/api_form.html
new file mode 100644
index 000000000..96f924ed8
--- /dev/null
+++ b/rest_framework/templates/rest_framework/api_form.html
@@ -0,0 +1,8 @@
+{% load rest_framework %}
+{% csrf_token %}
+{% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+{% endfor %}
+
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 9d939e738..877387f28 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -1,236 +1,259 @@
{% load url from future %}
+{% load staticfiles %}
{% load rest_framework %}
- {% block head %}
+ {% block head %}
+
+ {% block meta %}
+
+
+ {% endblock %}
+
+ {% block title %}Django REST framework{% endblock %}
+
+ {% block style %}
+ {% block bootstrap_theme %}
+
+
+ {% endblock %}
+
+
+ {% endblock %}
- {% block meta %}
-
-
{% endblock %}
-
- {% block title %}Django REST framework{% endblock %}
-
- {% block style %}
- {% block bootstrap_theme %}
-
-
- {% endblock %}
-
-
- {% endblock %}
-
- {% endblock %}
-
+ {% block body %}
+
-
+
- {% block navbar %}
-
-
-
-
-
-
-
-
-
- {% block footer %}
-
+ {% block script %}
+
+
+
+
+ {% endblock %}
+
{% endblock %}
-
- {% block script %}
-
-
-
-
- {% endblock %}
-
diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html
deleted file mode 100644
index b27f652e9..000000000
--- a/rest_framework/templates/rest_framework/form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% load rest_framework %}
-{% csrf_token %}
-{{ form.non_field_errors }}
-{% for field in form %}
-
- {{ field.label_tag|add_class:"control-label" }}
-
- {{ field }}
- {{ field.help_text }}
-
-
-
-{% endfor %}
diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox.html b/rest_framework/templates/rest_framework/horizontal/checkbox.html
new file mode 100644
index 000000000..07a7308f0
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/checkbox.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html
new file mode 100644
index 000000000..ec7d59356
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html
@@ -0,0 +1,30 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if style.inline %}
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/fieldset.html b/rest_framework/templates/rest_framework/horizontal/fieldset.html
new file mode 100644
index 000000000..ba3e3abae
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/fieldset.html
@@ -0,0 +1,13 @@
+{% load rest_framework %}
+
diff --git a/rest_framework/templates/rest_framework/horizontal/form.html b/rest_framework/templates/rest_framework/horizontal/form.html
new file mode 100644
index 000000000..fd15b6261
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/form.html
@@ -0,0 +1,15 @@
+{% load rest_framework %}
+
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/input.html b/rest_framework/templates/rest_framework/horizontal/input.html
new file mode 100644
index 000000000..c41cd523a
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/input.html
@@ -0,0 +1,14 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
new file mode 100644
index 000000000..a9ff04a62
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
@@ -0,0 +1,16 @@
+{% load rest_framework %}
+
diff --git a/rest_framework/templates/rest_framework/horizontal/radio.html b/rest_framework/templates/rest_framework/horizontal/radio.html
new file mode 100644
index 000000000..52238bb1a
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/radio.html
@@ -0,0 +1,30 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if style.inline %}
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
new file mode 100644
index 000000000..8a7fca370
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -0,0 +1,21 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
new file mode 100644
index 000000000..0735f2809
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+
+ {% if field.label %}
+
+ {% endif %}
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/horizontal/textarea.html b/rest_framework/templates/rest_framework/horizontal/textarea.html
new file mode 100644
index 000000000..ec1075495
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/textarea.html
@@ -0,0 +1,14 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/inline/checkbox.html b/rest_framework/templates/rest_framework/inline/checkbox.html
new file mode 100644
index 000000000..71737f15f
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/checkbox.html
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
new file mode 100644
index 000000000..093496862
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
@@ -0,0 +1,13 @@
+
+ {% if field.label %}
+
+ {% endif %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+
diff --git a/rest_framework/templates/rest_framework/inline/fieldset.html b/rest_framework/templates/rest_framework/inline/fieldset.html
new file mode 100644
index 000000000..e49b42fdf
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/fieldset.html
@@ -0,0 +1,6 @@
+{% load rest_framework %}
+{% for nested_field in field %}
+ {% if not nested_field.read_only %}
+ {% render_field nested_field style=style %}
+ {% endif %}
+{% endfor %}
diff --git a/rest_framework/templates/rest_framework/inline/form.html b/rest_framework/templates/rest_framework/inline/form.html
new file mode 100644
index 000000000..6a0ea81d3
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/form.html
@@ -0,0 +1,11 @@
+{% load rest_framework %}
+
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+
+
+
diff --git a/rest_framework/templates/rest_framework/inline/input.html b/rest_framework/templates/rest_framework/inline/input.html
new file mode 100644
index 000000000..de85ba485
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/input.html
@@ -0,0 +1,6 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/inline/list_fieldset.html b/rest_framework/templates/rest_framework/inline/list_fieldset.html
new file mode 100644
index 000000000..2ae56d7cd
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/list_fieldset.html
@@ -0,0 +1 @@
+Lists are not currently supported in HTML input.
diff --git a/rest_framework/templates/rest_framework/inline/radio.html b/rest_framework/templates/rest_framework/inline/radio.html
new file mode 100644
index 000000000..1915f4f84
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/radio.html
@@ -0,0 +1,13 @@
+
+ {% if field.label %}
+
+ {% endif %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html
new file mode 100644
index 000000000..6b30e4d6b
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/select.html
@@ -0,0 +1,13 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html
new file mode 100644
index 000000000..5a8b2494b
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/select_multiple.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+
+ {% if field.label %}
+
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/inline/textarea.html b/rest_framework/templates/rest_framework/inline/textarea.html
new file mode 100644
index 000000000..0766a01c5
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/textarea.html
@@ -0,0 +1,6 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index be9a0072a..8e6240a68 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -1,17 +1,9 @@
+{% extends "rest_framework/base.html" %}
{% load url from future %}
+{% load staticfiles %}
{% load rest_framework %}
-
-
-
- {% block style %}
- {% block bootstrap_theme %}
-
-
- {% endblock %}
-
- {% endblock %}
-
+ {% block body %}
@@ -25,23 +17,46 @@
-
+
{% csrf_token %}
-
-
-
-
+
+
+
+
+ {% if form.username.errors %}
+
+ {{ form.username.errors|striptags }}
+
+ {% endif %}
-
-
-
-
+
+
+
+
+ {% if form.password.errors %}
+
+ {{ form.password.errors|striptags }}
+
+ {% endif %}
+ {% if form.non_field_errors %}
+ {% for error in form.non_field_errors %}
+ {{ error }}
+ {% endfor %}
+ {% endif %}
-
+
@@ -50,4 +65,4 @@
-
+ {% endblock %}
diff --git a/rest_framework/templates/rest_framework/pagination/numbers.html b/rest_framework/templates/rest_framework/pagination/numbers.html
new file mode 100644
index 000000000..040458104
--- /dev/null
+++ b/rest_framework/templates/rest_framework/pagination/numbers.html
@@ -0,0 +1,27 @@
+
+ {% if previous_url %}
+
+ {% else %}
+
+ {% endif %}
+
+ {% for page_link in page_links %}
+ {% if page_link.is_break %}
+ -
+
+
+ {% else %}
+ {% if page_link.is_active %}
+ - {{ page_link.number }}
+ {% else %}
+ - {{ page_link.number }}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+
+ {% if next_url %}
+
+ {% else %}
+
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/pagination/previous_and_next.html b/rest_framework/templates/rest_framework/pagination/previous_and_next.html
new file mode 100644
index 000000000..eacbfff45
--- /dev/null
+++ b/rest_framework/templates/rest_framework/pagination/previous_and_next.html
@@ -0,0 +1,12 @@
+
+{% if previous_url %}
+ - « Previous
+{% else %}
+ - « Previous
+{% endif %}
+{% if next_url %}
+ - Next »
+{% else %}
+ - Next »
+{% endif %}
+
diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html
new file mode 100644
index 000000000..b4c9f1a11
--- /dev/null
+++ b/rest_framework/templates/rest_framework/raw_data_form.html
@@ -0,0 +1,12 @@
+{% load rest_framework %}
+{% csrf_token %}
+{{ form.non_field_errors }}
+{% for field in form %}
+
+ {{ field.label_tag|add_class:"col-sm-2 control-label" }}
+
+ {{ field|add_class:"form-control" }}
+ {{ field.help_text }}
+
+
+{% endfor %}
diff --git a/rest_framework/templates/rest_framework/vertical/checkbox.html b/rest_framework/templates/rest_framework/vertical/checkbox.html
new file mode 100644
index 000000000..e21a8e902
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/checkbox.html
@@ -0,0 +1,14 @@
+
+
+
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
\ No newline at end of file
diff --git a/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html
new file mode 100644
index 000000000..134cca661
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html
@@ -0,0 +1,30 @@
+
+ {% if field.label %}
+
+ {% endif %}
+ {% if style.inline %}
+
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/fieldset.html b/rest_framework/templates/rest_framework/vertical/fieldset.html
new file mode 100644
index 000000000..3eb5191c7
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/fieldset.html
@@ -0,0 +1,9 @@
+{% load rest_framework %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/form.html b/rest_framework/templates/rest_framework/vertical/form.html
new file mode 100644
index 000000000..e68835c0e
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/form.html
@@ -0,0 +1,11 @@
+{% load rest_framework %}
+
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+
+
+
diff --git a/rest_framework/templates/rest_framework/vertical/input.html b/rest_framework/templates/rest_framework/vertical/input.html
new file mode 100644
index 000000000..43cccd3ea
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/input.html
@@ -0,0 +1,12 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
new file mode 100644
index 000000000..82d7b5f41
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -0,0 +1,4 @@
+
diff --git a/rest_framework/templates/rest_framework/vertical/radio.html b/rest_framework/templates/rest_framework/vertical/radio.html
new file mode 100644
index 000000000..ed9f9ddbb
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/radio.html
@@ -0,0 +1,30 @@
+
+ {% if field.label %}
+
+ {% endif %}
+ {% if style.inline %}
+
+ {% for key, text in field.choices.items %}
+
+ {% endfor %}
+
+ {% else %}
+ {% for key, text in field.choices.items %}
+
+
+
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
new file mode 100644
index 000000000..1d1109f6e
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -0,0 +1,19 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html
new file mode 100644
index 000000000..81b25c2a3
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html
@@ -0,0 +1,21 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templates/rest_framework/vertical/textarea.html b/rest_framework/templates/rest_framework/vertical/textarea.html
new file mode 100644
index 000000000..840ea853c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/textarea.html
@@ -0,0 +1,12 @@
+
+ {% if field.label %}
+
+ {% endif %}
+
+ {% if field.errors %}
+ {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ {{ field.help_text }}
+ {% endif %}
+
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index e9c1cdd54..bf0dc7b8f 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -1,115 +1,32 @@
from __future__ import unicode_literals, absolute_import
from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
-from django.http import QueryDict
+from django.utils import six
+from django.utils.encoding import iri_to_uri, force_text
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
-from rest_framework.compat import urlparse, force_text, six, smart_urlquote
-import re, string
+from django.utils.html import smart_urlquote
+from rest_framework.renderers import HTMLFormRenderer
+from rest_framework.utils.urls import replace_query_param
+import re
register = template.Library()
-
-# Note we don't use 'load staticfiles', because we need a 1.3 compatible
-# version, so instead we include the `static` template tag ourselves.
-
-# When 1.3 becomes unsupported by REST framework, we can instead start to
-# use the {% load staticfiles %} tag, remove the following code,
-# and add a dependency that `django.contrib.staticfiles` must be installed.
-
-# Note: We can't put this into the `compat` module because the compat import
-# from rest_framework.compat import ...
-# conflicts with this rest_framework template tag module.
-
-try: # Django 1.5+
- from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
-
- @register.tag('static')
- def do_static(parser, token):
- return StaticFilesNode.handle_token(parser, token)
-
-except ImportError:
- try: # Django 1.4
- from django.contrib.staticfiles.storage import staticfiles_storage
-
- @register.simple_tag
- def static(path):
- """
- A template tag that returns the URL to a file
- using staticfiles' storage backend
- """
- return staticfiles_storage.url(path)
-
- except ImportError: # Django 1.3
- from urlparse import urljoin
- from django import template
- from django.templatetags.static import PrefixNode
-
- class StaticNode(template.Node):
- def __init__(self, varname=None, path=None):
- if path is None:
- raise template.TemplateSyntaxError(
- "Static template nodes must be given a path to return.")
- self.path = path
- self.varname = varname
-
- def url(self, context):
- path = self.path.resolve(context)
- return self.handle_simple(path)
-
- def render(self, context):
- url = self.url(context)
- if self.varname is None:
- return url
- context[self.varname] = url
- return ''
-
- @classmethod
- def handle_simple(cls, path):
- return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
-
- @classmethod
- def handle_token(cls, parser, token):
- """
- Class method to parse prefix node and return a Node.
- """
- bits = token.split_contents()
-
- if len(bits) < 2:
- raise template.TemplateSyntaxError(
- "'%s' takes at least one argument (path to file)" % bits[0])
-
- path = parser.compile_filter(bits[1])
-
- if len(bits) >= 2 and bits[-2] == 'as':
- varname = bits[3]
- else:
- varname = None
-
- return cls(varname, path)
-
- @register.tag('static')
- def do_static_13(parser, token):
- return StaticNode.handle_token(parser, token)
-
-
-def replace_query_param(url, key, val):
- """
- Given a URL and a key/val pair, set or replace an item in the query
- parameters of the URL, and return the new URL.
- """
- (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
- query_dict = QueryDict(query).copy()
- query_dict[key] = val
- query = query_dict.urlencode()
- return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
-
-
# Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
-# And the template tags themselves...
+@register.simple_tag
+def get_pagination_html(pager):
+ return pager.to_html()
+
+
+@register.simple_tag
+def render_field(field, style=None):
+ style = style or {}
+ renderer = style.get('renderer', HTMLFormRenderer())
+ return renderer.render_field(field, style)
+
@register.simple_tag
def optional_login(request):
@@ -121,22 +38,31 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "Log in" % (login_url, request.path)
+ snippet = "Log in ".format(href=login_url, next=escape(request.path))
return snippet
@register.simple_tag
-def optional_logout(request):
+def optional_logout(request, user):
"""
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
try:
logout_url = reverse('rest_framework:logout')
except NoReverseMatch:
- return ''
+ return '{user} '.format(user=user)
- snippet = "Log out" % (logout_url, request.path)
- return snippet
+ snippet = """
+
+ {user}
+
+
+
+ """
+
+ return snippet.format(user=user, href=logout_url, next=escape(request.path))
@register.simple_tag
@@ -144,7 +70,9 @@ def add_query_param(request, key, val):
"""
Add a query parameter to the current request url, and return the new url.
"""
- return replace_query_param(request.get_full_path(), key, val)
+ iri = request.get_full_path()
+ uri = iri_to_uri(iri)
+ return escape(replace_query_param(uri, key, val))
@register.filter
@@ -177,7 +105,7 @@ def add_class(value, css_class):
# Bunch of stuff cloned from urlize
-TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
+TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"]
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'),
('"', '"'), ("'", "'")]
word_split_re = re.compile(r'(\s+)')
@@ -186,6 +114,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
+def smart_urlquote_wrapper(matched_url):
+ """
+ Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
+ be raised here, see issue #1386
+ """
+ try:
+ return smart_urlquote(matched_url)
+ except ValueError:
+ return None
+
+
@register.filter
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
"""
@@ -204,11 +143,12 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
If autoescape is True, the link text and URLs will get autoescaped.
"""
- trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ def trim_url(x, limit=trim_url_limit):
+ return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+
safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_text(text))
for i, word in enumerate(words):
- match = None
if '.' in word or '@' in word or ':' in word:
# Deal with punctuation.
lead, middle, trail = '', word, ''
@@ -221,8 +161,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
middle = middle[len(opening):]
lead = lead + opening
# Keep parentheses at the end only if they're balanced.
- if (middle.endswith(closing)
- and middle.count(closing) == middle.count(opening) + 1):
+ if (
+ middle.endswith(closing) and
+ middle.count(closing) == middle.count(opening) + 1
+ ):
middle = middle[:-len(closing)]
trail = closing + trail
@@ -230,10 +172,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
url = None
nofollow_attr = ' rel="nofollow"' if nofollow else ''
if simple_url_re.match(middle):
- url = smart_urlquote(middle)
+ url = smart_urlquote_wrapper(middle)
elif simple_url_2_re.match(middle):
- url = smart_urlquote('http://%s' % middle)
- elif not ':' in middle and simple_email_re.match(middle):
+ url = smart_urlquote_wrapper('http://%s' % middle)
+ elif ':' not in middle and simple_email_re.match(middle):
local, domain = middle.rsplit('@', 1)
try:
domain = domain.encode('idna').decode('ascii')
diff --git a/rest_framework/test.py b/rest_framework/test.py
index a18f5a293..a83d082ab 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -8,9 +8,11 @@ from django.conf import settings
from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler
from django.test import testcases
+from django.utils import six
+from django.utils.http import urlencode
from rest_framework.settings import api_settings
from rest_framework.compat import RequestFactory as DjangoRequestFactory
-from rest_framework.compat import force_bytes_or_smart_bytes, six
+from rest_framework.compat import force_bytes_or_smart_bytes
def force_authenticate(request, user=None, token=None):
@@ -34,8 +36,8 @@ class APIRequestFactory(DjangoRequestFactory):
Encode the data returning a two tuple of (bytes, content_type)
"""
- if not data:
- return ('', None)
+ if data is None:
+ return ('', content_type)
assert format is None or content_type is None, (
'You may not set both `format` and `content_type`.'
@@ -48,9 +50,10 @@ class APIRequestFactory(DjangoRequestFactory):
else:
format = format or self.default_format
- assert format in self.renderer_classes, ("Invalid format '{0}'. "
- "Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
- "to enable extra request formats.".format(
+ assert format in self.renderer_classes, (
+ "Invalid format '{0}'. Available formats are {1}. "
+ "Set TEST_REQUEST_RENDERER_CLASSES to enable "
+ "extra request formats.".format(
format,
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
)
@@ -71,6 +74,17 @@ class APIRequestFactory(DjangoRequestFactory):
return ret, content_type
+ def get(self, path, data=None, **extra):
+ r = {
+ 'QUERY_STRING': urlencode(data or {}, doseq=True),
+ }
+ # Fix to support old behavior where you have the arguments in the url
+ # See #1461
+ if not data and '?' in path:
+ r['QUERY_STRING'] = path.split('?')[1]
+ r.update(extra)
+ return self.generic('GET', path, **r)
+
def post(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('POST', path, data, content_type, **extra)
@@ -134,12 +148,70 @@ class APIClient(APIRequestFactory, DjangoClient):
"""
self.handler._force_user = user
self.handler._force_token = token
+ if user is None:
+ self.logout() # Also clear any possible session info if required
def request(self, **kwargs):
# Ensure that any credentials set get added to every request.
kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs)
+ def get(self, path, data=None, follow=False, **extra):
+ response = super(APIClient, self).get(path, data=data, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def post(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).post(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def put(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).put(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def patch(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).patch(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def delete(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).delete(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def options(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).options(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def logout(self):
+ self._credentials = {}
+
+ # Also clear any `force_authenticate`
+ self.handler._force_user = None
+ self.handler._force_token = None
+
+ if self.session:
+ super(APIClient, self).logout()
+
class APITransactionTestCase(testcases.TransactionTestCase):
client_class = APIClient
diff --git a/rest_framework/tests/extras/bad_import.py b/rest_framework/tests/extras/bad_import.py
deleted file mode 100644
index 68263d947..000000000
--- a/rest_framework/tests/extras/bad_import.py
+++ /dev/null
@@ -1 +0,0 @@
-raise ValueError
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
deleted file mode 100644
index 1598ecd94..000000000
--- a/rest_framework/tests/models.py
+++ /dev/null
@@ -1,169 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
-
-
-def foobar():
- return 'foobar'
-
-
-class CustomField(models.CharField):
-
- def __init__(self, *args, **kwargs):
- kwargs['max_length'] = 12
- super(CustomField, self).__init__(*args, **kwargs)
-
-
-class RESTFrameworkModel(models.Model):
- """
- Base for test models that sets app_label, so they play nicely.
- """
- class Meta:
- app_label = 'tests'
- abstract = True
-
-
-class HasPositiveIntegerAsChoice(RESTFrameworkModel):
- some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
- some_integer = models.PositiveIntegerField(choices=some_choices)
-
-
-class Anchor(RESTFrameworkModel):
- text = models.CharField(max_length=100, default='anchor')
-
-
-class BasicModel(RESTFrameworkModel):
- text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
-
-
-class SlugBasedModel(RESTFrameworkModel):
- text = models.CharField(max_length=100)
- slug = models.SlugField(max_length=32)
-
-
-class DefaultValueModel(RESTFrameworkModel):
- text = models.CharField(default='foobar', max_length=100)
- extra = models.CharField(blank=True, null=True, max_length=100)
-
-
-class CallableDefaultValueModel(RESTFrameworkModel):
- text = models.CharField(default=foobar, max_length=100)
-
-
-class ManyToManyModel(RESTFrameworkModel):
- rel = models.ManyToManyField(Anchor, help_text='Some help text.')
-
-
-class ReadOnlyManyToManyModel(RESTFrameworkModel):
- text = models.CharField(max_length=100, default='anchor')
- rel = models.ManyToManyField(Anchor)
-
-
-# Model for regression test for #285
-
-class Comment(RESTFrameworkModel):
- email = models.EmailField()
- content = models.CharField(max_length=200)
- created = models.DateTimeField(auto_now_add=True)
-
-
-class ActionItem(RESTFrameworkModel):
- title = models.CharField(max_length=200)
- done = models.BooleanField(default=False)
- info = CustomField(default='---', max_length=12)
-
-
-# Models for reverse relations
-class Person(RESTFrameworkModel):
- name = models.CharField(max_length=10)
- age = models.IntegerField(null=True, blank=True)
-
- @property
- def info(self):
- return {
- 'name': self.name,
- 'age': self.age,
- }
-
-
-class BlogPost(RESTFrameworkModel):
- title = models.CharField(max_length=100)
- writer = models.ForeignKey(Person, null=True, blank=True)
-
- def get_first_comment(self):
- return self.blogpostcomment_set.all()[0]
-
-
-class BlogPostComment(RESTFrameworkModel):
- text = models.TextField()
- blog_post = models.ForeignKey(BlogPost)
-
-
-class Album(RESTFrameworkModel):
- title = models.CharField(max_length=100, unique=True)
-
-
-class Photo(RESTFrameworkModel):
- description = models.TextField()
- album = models.ForeignKey(Album)
-
-
-# Model for issue #324
-class BlankFieldModel(RESTFrameworkModel):
- title = models.CharField(max_length=100, blank=True, null=False)
-
-
-# Model for issue #380
-class OptionalRelationModel(RESTFrameworkModel):
- other = models.ForeignKey('OptionalRelationModel', blank=True, null=True)
-
-
-# Model for RegexField
-class Book(RESTFrameworkModel):
- isbn = models.CharField(max_length=13)
-
-
-# Models for relations tests
-# ManyToMany
-class ManyToManyTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class ManyToManySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
-
-
-# ForeignKey
-class ForeignKeyTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class ForeignKeySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
-
-
-# Nullable ForeignKey
-class NullableForeignKeySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
-
-
-# OneToOne
-class OneToOneTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class NullableOneToOneSource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
- related_name='nullable_source')
-
-
-# Serializer used to test BasicModel
-class BasicModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
deleted file mode 100644
index a44813b69..000000000
--- a/rest_framework/tests/test_authentication.py
+++ /dev/null
@@ -1,635 +0,0 @@
-from __future__ import unicode_literals
-from django.contrib.auth.models import User
-from django.http import HttpResponse
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import HTTP_HEADER_ENCODING
-from rest_framework import exceptions
-from rest_framework import permissions
-from rest_framework import renderers
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.authentication import (
- BaseAuthentication,
- TokenAuthentication,
- BasicAuthentication,
- SessionAuthentication,
- OAuthAuthentication,
- OAuth2Authentication
-)
-from rest_framework.authtoken.models import Token
-from rest_framework.compat import patterns, url, include
-from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
-from rest_framework.compat import oauth, oauth_provider
-from rest_framework.test import APIRequestFactory, APIClient
-from rest_framework.views import APIView
-import base64
-import time
-import datetime
-
-factory = APIRequestFactory()
-
-
-class MockView(APIView):
- permission_classes = (permissions.IsAuthenticated,)
-
- def get(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
- def post(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
- def put(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
-
-urlpatterns = patterns('',
- (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
- (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
- (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
- (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
- (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
- (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
- permission_classes=[permissions.TokenHasReadWriteScope]))
-)
-
-if oauth2_provider is not None:
- urlpatterns += patterns('',
- url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
- url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
- url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
- permission_classes=[permissions.TokenHasReadWriteScope])),
- )
-
-
-class BasicAuthTests(TestCase):
- """Basic authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- def test_post_form_passing_basic_auth(self):
- """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
- credentials = ('%s:%s' % (self.username, self.password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- auth = 'Basic %s' % base64_credentials
- response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_json_passing_basic_auth(self):
- """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
- credentials = ('%s:%s' % (self.username, self.password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- auth = 'Basic %s' % base64_credentials
- response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_failing_basic_auth(self):
- """Ensure POSTing form over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/basic/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_post_json_failing_basic_auth(self):
- """Ensure POSTing json over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
- self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
-
-
-class SessionAuthTests(TestCase):
- """User session authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.non_csrf_client = APIClient(enforce_csrf_checks=False)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- def tearDown(self):
- self.csrf_client.logout()
-
- def test_post_form_session_auth_failing_csrf(self):
- """
- Ensure POSTing form over session authentication without CSRF token fails.
- """
- self.csrf_client.login(username=self.username, password=self.password)
- response = self.csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_post_form_session_auth_passing(self):
- """
- Ensure POSTing form over session authentication with logged in user and CSRF token passes.
- """
- self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_put_form_session_auth_passing(self):
- """
- Ensure PUTting form over session authentication with logged in user and CSRF token passes.
- """
- self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.put('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_session_auth_failing(self):
- """
- Ensure POSTing form over session authentication without logged in user fails.
- """
- response = self.csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-
-class TokenAuthTests(TestCase):
- """Token authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.key = 'abcd1234'
- self.token = Token.objects.create(key=self.key, user=self.user)
-
- def test_post_form_passing_token_auth(self):
- """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
- auth = 'Token ' + self.key
- response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_json_passing_token_auth(self):
- """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
- auth = "Token " + self.key
- response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_failing_token_auth(self):
- """Ensure POSTing form over token auth without correct credentials fails"""
- response = self.csrf_client.post('/token/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_post_json_failing_token_auth(self):
- """Ensure POSTing json over token auth without correct credentials fails"""
- response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_token_has_auto_assigned_key_if_none_provided(self):
- """Ensure creating a token with no key will auto-assign a key"""
- self.token.delete()
- token = Token.objects.create(user=self.user)
- self.assertTrue(bool(token.key))
-
- def test_token_login_json(self):
- """Ensure token login view using JSON POST works."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': self.password}, format='json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['token'], self.key)
-
- def test_token_login_json_bad_creds(self):
- """Ensure token login view using JSON POST fails if bad credentials are used."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': "badpass"}, format='json')
- self.assertEqual(response.status_code, 400)
-
- def test_token_login_json_missing_fields(self):
- """Ensure token login view using JSON POST fails if missing fields."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username}, format='json')
- self.assertEqual(response.status_code, 400)
-
- def test_token_login_form(self):
- """Ensure token login view using form POST works."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': self.password})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['token'], self.key)
-
-
-class IncorrectCredentialsTests(TestCase):
- def test_incorrect_credentials(self):
- """
- If a request contains bad authentication credentials, then
- authentication should run and error, even if no permissions
- are set on the view.
- """
- class IncorrectCredentialsAuth(BaseAuthentication):
- def authenticate(self, request):
- raise exceptions.AuthenticationFailed('Bad credentials')
-
- request = factory.get('/')
- view = MockView.as_view(
- authentication_classes=(IncorrectCredentialsAuth,),
- permission_classes=()
- )
- response = view(request)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- self.assertEqual(response.data, {'detail': 'Bad credentials'})
-
-
-class OAuthTests(TestCase):
- """OAuth 1.0a authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- # these imports are here because oauth is optional and hiding them in try..except block or compat
- # could obscure problems if something breaks
- from oauth_provider.models import Consumer, Resource
- from oauth_provider.models import Token as OAuthToken
- from oauth_provider import consts
-
- self.consts = consts
-
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CONSUMER_KEY = 'consumer_key'
- self.CONSUMER_SECRET = 'consumer_secret'
- self.TOKEN_KEY = "token_key"
- self.TOKEN_SECRET = "token_secret"
-
- self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
- name='example', user=self.user, status=self.consts.ACCEPTED)
-
- self.resource = Resource.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource,
- token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
- )
-
- def _create_authorization_header(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
-
- return req.to_header()["Authorization"]
-
- def _create_authorization_url_parameters(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
- return dict(req)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_passing_oauth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_repeated_nonce_failing_oauth(self):
- """Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- # simulate reply attack auth header containes already used (nonce, timestamp) pair
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_token_removed_failing_oauth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_consumer_status_not_accepted_failing_oauth(self):
- """Ensure POSTing when consumer status is anything other than ACCEPTED fails"""
- for consumer_status in (self.consts.CANCELED, self.consts.PENDING, self.consts.REJECTED):
- self.consumer.status = consumer_status
- self.consumer.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_request_token_failing_oauth(self):
- """Ensure POSTing with unauthorized request token instead of access token fails"""
- self.token.token_type = self.token.REQUEST
- self.token.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_urlencoded_parameters(self):
- """Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_url_parameters(self):
- """Ensure GETing with auth in url parameters passes"""
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_hmac_sha1_signature_passes(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_readonly_resource_passing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth-with-scope/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_readonly_resource_failing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_write_resource_passing_auth(self):
- """Ensure POSTing with a write resource succeed"""
- read_write_access_token = self.token
- read_write_access_token.resource.is_readonly = False
- read_write_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_consumer_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': 'badconsumerkey'
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_token_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': 'badtokenkey',
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
-
-class OAuth2Tests(TestCase):
- """OAuth 2.0 authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CLIENT_ID = 'client_key'
- self.CLIENT_SECRET = 'client_secret'
- self.ACCESS_TOKEN = "access_token"
- self.REFRESH_TOKEN = "refresh_token"
-
- self.oauth2_client = oauth2_provider_models.Client.objects.create(
- client_id=self.CLIENT_ID,
- client_secret=self.CLIENT_SECRET,
- redirect_uri='',
- client_type=0,
- name='example',
- user=None,
- )
-
- self.access_token = oauth2_provider_models.AccessToken.objects.create(
- token=self.ACCESS_TOKEN,
- client=self.oauth2_client,
- user=self.user,
- )
- self.refresh_token = oauth2_provider_models.RefreshToken.objects.create(
- user=self.user,
- access_token=self.access_token,
- client=self.oauth2_client
- )
-
- def _create_authorization_header(self, token=None):
- return "Bearer {0}".format(token or self.access_token.token)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_type_failing(self):
- """Ensure that a wrong token type lead to the correct HTTP error status code"""
- auth = "Wrong token-type-obsviously"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_format_failing(self):
- """Ensure that a wrong token format lead to the correct HTTP error status code"""
- auth = "Bearer wrong token format"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_failing(self):
- """Ensure that a wrong token lead to the correct HTTP error status code"""
- auth = "Bearer wrong-token"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_passing_auth(self):
- """Ensure GETing form over OAuth with correct client credentials succeed"""
- auth = self._create_authorization_header()
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_passing_auth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_token_removed_failing_auth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.access_token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_refresh_token_failing_auth(self):
- """Ensure POSTing with refresh token instead of access token fails"""
- auth = self._create_authorization_header(token=self.refresh_token.token)
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_expired_access_token_failing_auth(self):
- """Ensure POSTing with expired access token fails with an 'Invalid token' error"""
- self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
- self.access_token.save()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
- self.assertIn('Invalid token', response.content)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_invalid_scope_failing_auth(self):
- """Ensure POSTing with a readonly scope instead of a write scope fails"""
- read_only_access_token = self.access_token
- read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
- read_only_access_token.save()
- auth = self._create_authorization_header(token=read_only_access_token.token)
- response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_valid_scope_passing_auth(self):
- """Ensure POSTing with a write scope succeed"""
- read_write_access_token = self.access_token
- read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
- read_write_access_token.save()
- auth = self._create_authorization_header(token=read_write_access_token.token)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
-
-class FailingAuthAccessedInRenderer(TestCase):
- def setUp(self):
- class AuthAccessingRenderer(renderers.BaseRenderer):
- media_type = 'text/plain'
- format = 'txt'
-
- def render(self, data, media_type=None, renderer_context=None):
- request = renderer_context['request']
- if request.user.is_authenticated():
- return b'authenticated'
- return b'not authenticated'
-
- class FailingAuth(BaseAuthentication):
- def authenticate(self, request):
- raise exceptions.AuthenticationFailed('authentication failed')
-
- class ExampleView(APIView):
- authentication_classes = (FailingAuth,)
- renderer_classes = (AuthAccessingRenderer,)
-
- def get(self, request):
- return Response({'foo': 'bar'})
-
- self.view = ExampleView.as_view()
-
- def test_failing_auth_accessed_in_renderer(self):
- """
- When authentication fails the renderer should still be able to access
- `request.user` without raising an exception. Particularly relevant
- to HTML responses that might reasonably access `request.user`.
- """
- request = factory.get('/')
- response = self.view(request)
- content = response.render().content
- self.assertEqual(content, b'not authenticated')
diff --git a/rest_framework/tests/test_breadcrumbs.py b/rest_framework/tests/test_breadcrumbs.py
deleted file mode 100644
index 41ddf2cea..000000000
--- a/rest_framework/tests/test_breadcrumbs.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework.compat import patterns, url
-from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.views import APIView
-
-
-class Root(APIView):
- pass
-
-
-class ResourceRoot(APIView):
- pass
-
-
-class ResourceInstance(APIView):
- pass
-
-
-class NestedResourceRoot(APIView):
- pass
-
-
-class NestedResourceInstance(APIView):
- pass
-
-urlpatterns = patterns('',
- url(r'^$', Root.as_view()),
- url(r'^resource/$', ResourceRoot.as_view()),
- url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()),
- url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()),
- url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()),
-)
-
-
-class BreadcrumbTests(TestCase):
- """Tests the breadcrumb functionality used by the HTML renderer."""
-
- urls = 'rest_framework.tests.test_breadcrumbs'
-
- def test_root_breadcrumbs(self):
- url = '/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
-
- def test_resource_root_breadcrumbs(self):
- url = '/resource/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/')])
-
- def test_resource_instance_breadcrumbs(self):
- url = '/resource/123'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123')])
-
- def test_nested_resource_breadcrumbs(self):
- url = '/resource/123/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/')])
-
- def test_nested_resource_instance_breadcrumbs(self):
- url = '/resource/123/abc'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/'),
- ('Nested Resource Instance', '/resource/123/abc')])
-
- def test_broken_url_breadcrumbs_handled_gracefully(self):
- url = '/foobar'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
deleted file mode 100644
index 6836ec86f..000000000
--- a/rest_framework/tests/test_fields.py
+++ /dev/null
@@ -1,898 +0,0 @@
-"""
-General serializer field tests.
-"""
-from __future__ import unicode_literals
-
-import datetime
-from decimal import Decimal
-from uuid import uuid4
-from django.core import validators
-from django.db import models
-from django.test import TestCase
-from django.utils.datastructures import SortedDict
-from rest_framework import serializers
-from rest_framework.tests.models import RESTFrameworkModel
-
-
-class TimestampedModel(models.Model):
- added = models.DateTimeField(auto_now_add=True)
- updated = models.DateTimeField(auto_now=True)
-
-
-class CharPrimaryKeyModel(models.Model):
- id = models.CharField(max_length=20, primary_key=True)
-
-
-class TimestampedModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimestampedModel
-
-
-class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = CharPrimaryKeyModel
-
-
-class TimeFieldModel(models.Model):
- clock = models.TimeField()
-
-
-class TimeFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimeFieldModel
-
-
-class BasicFieldTests(TestCase):
- def test_auto_now_fields_read_only(self):
- """
- auto_now and auto_now_add fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['added'].read_only, True)
-
- def test_auto_pk_fields_read_only(self):
- """
- AutoField fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, True)
-
- def test_non_auto_pk_fields_not_read_only(self):
- """
- PK fields other than AutoField fields should not be read_only by default.
- """
- serializer = CharPrimaryKeyModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, False)
-
- def test_dict_field_ordering(self):
- """
- Field should preserve dictionary ordering, if it exists.
- See: https://github.com/tomchristie/django-rest-framework/issues/832
- """
- ret = SortedDict()
- ret['c'] = 1
- ret['b'] = 1
- ret['a'] = 1
- ret['z'] = 1
- field = serializers.Field()
- keys = list(field.to_native(ret).keys())
- self.assertEqual(keys, ['c', 'b', 'a', 'z'])
-
-
-class DateFieldTest(TestCase):
- """
- Tests for the DateFieldTest from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateField()
- result_1 = f.from_native('1984-07-31')
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_from_native_datetime_date(self):
- """
- Make sure from_native() accepts a datetime.date instance.
- """
- f = serializers.DateField()
- result_1 = f.from_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(result_1, datetime.date(1984, 7, 31))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
- result = f.from_native('1984 -- 31')
-
- self.assertEqual(datetime.date(1984, 1, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
-
- try:
- f.from_native('1984-07-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_date(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid date.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984-13-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns datetime as default.
- """
- f = serializers.DateField()
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with 'iso-8601' returns iso formated date.
- """
- f = serializers.DateField(format='iso-8601')
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984-07-31', result_1)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateField(format="%Y - %m.%d")
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984 - 07.31', result_1)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class DateTimeFieldTest(TestCase):
- """
- Tests for the DateTimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native('1984-07-31 04:31')
- result_2 = f.from_native('1984-07-31 04:31:59')
- result_3 = f.from_native('1984-07-31 04:31:59.000200')
-
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_datetime(self):
- """
- Make sure from_native() accepts a datetime.datetime instance.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31))
- self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59))
- self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
- result = f.from_native('1984 -- 04:59')
-
- self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
-
- try:
- f.from_native('1984-07-31 04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateTimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_datetime(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid datetime.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns isoformat as default.
- """
- f = serializers.DateTimeField()
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(datetime.datetime(1984, 7, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format=iso-8601 returns iso formatted datetime.
- """
- f = serializers.DateTimeField(format='iso-8601')
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984-07-31T00:00:00', result_1)
- self.assertEqual('1984-07-31T04:31:00', result_2)
- self.assertEqual('1984-07-31T04:31:59', result_3)
- self.assertEqual('1984-07-31T04:31:59.000200', result_4)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateTimeField(format="%Y - %H:%M")
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984 - 00:00', result_1)
- self.assertEqual('1984 - 04:31', result_2)
- self.assertEqual('1984 - 04:31', result_3)
- self.assertEqual('1984 - 04:31', result_4)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class TimeFieldTest(TestCase):
- """
- Tests for the TimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.TimeField()
- result_1 = f.from_native('04:31')
- result_2 = f.from_native('04:31:59')
- result_3 = f.from_native('04:31:59.000200')
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_time(self):
- """
- Make sure from_native() accepts a datetime.time instance.
- """
- f = serializers.TimeField()
- result_1 = f.from_native(datetime.time(4, 31))
- result_2 = f.from_native(datetime.time(4, 31, 59))
- result_3 = f.from_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.time(4, 31))
- self.assertEqual(result_2, datetime.time(4, 31, 59))
- self.assertEqual(result_3, datetime.time(4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
- result = f.from_native('04 -- 31')
-
- self.assertEqual(datetime.time(4, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
-
- try:
- f.from_native('04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.TimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.TimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_time(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid time.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns time object as default.
- """
- f = serializers.TimeField()
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format='iso-8601' returns iso formatted time.
- """
- f = serializers.TimeField(format='iso-8601')
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04:31:00', result_1)
- self.assertEqual('04:31:59', result_2)
- self.assertEqual('04:31:59.000200', result_3)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.TimeField(format="%H - %S [%f]")
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04 - 00 [000000]', result_1)
- self.assertEqual('04 - 59 [000000]', result_2)
- self.assertEqual('04 - 59 [000200]', result_3)
-
-
-class DecimalFieldTest(TestCase):
- """
- Tests for the DecimalField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts string values
- """
- f = serializers.DecimalField()
- result_1 = f.from_native('9000')
- result_2 = f.from_native('1.00000001')
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_from_native_invalid_string(self):
- """
- Make sure from_native() raises ValidationError on passing invalid string
- """
- f = serializers.DecimalField()
-
- try:
- f.from_native('123.45.6')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Enter a number."])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_integer(self):
- """
- Make sure from_native() accepts integer values
- """
- f = serializers.DecimalField()
- result = f.from_native(9000)
-
- self.assertEqual(Decimal('9000'), result)
-
- def test_from_native_float(self):
- """
- Make sure from_native() accepts float values
- """
- f = serializers.DecimalField()
- result = f.from_native(1.00000001)
-
- self.assertEqual(Decimal('1.00000001'), result)
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DecimalField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_to_native(self):
- """
- Make sure to_native() returns Decimal as string.
- """
- f = serializers.DecimalField()
-
- result_1 = f.to_native(Decimal('9000'))
- result_2 = f.to_native(Decimal('1.00000001'))
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField(required=False)
- self.assertEqual(None, f.to_native(None))
-
- def test_valid_serialization(self):
- """
- Make sure the serializer works correctly
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=9010,
- min_value=9000,
- max_digits=6,
- decimal_places=2)
-
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
-
- self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
-
- def test_raise_max_value(self):
- """
- Make sure max_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '123'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
-
- def test_raise_min_value(self):
- """
- Make sure min_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(min_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '99'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
-
- def test_raise_max_digits(self):
- """
- Make sure max_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=5)
-
- s = DecimalSerializer(data={'decimal_field': '123.456'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
-
- def test_raise_max_decimal_places(self):
- """
- Make sure max_decimal_places violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '123.4567'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
-
- def test_raise_max_whole_digits(self):
- """
- Make sure max_whole_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '12345.6'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
-
-
-class ChoiceFieldTests(TestCase):
- """
- Tests for the ChoiceField options generator
- """
-
- SAMPLE_CHOICES = [
- ('red', 'Red'),
- ('green', 'Green'),
- ('blue', 'Blue'),
- ]
-
- def test_choices_required(self):
- """
- Make sure proper choices are rendered if field is required
- """
- f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, self.SAMPLE_CHOICES)
-
- def test_choices_not_required(self):
- """
- Make sure proper choices (plus blank) are rendered if the field isn't required
- """
- f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
-
-
-class EmailFieldTests(TestCase):
- """
- Tests for EmailField attribute values
- """
-
- class EmailFieldModel(RESTFrameworkModel):
- email_field = models.EmailField(blank=True)
-
- class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- email_field = models.EmailField(max_length=150, blank=True)
-
- def test_default_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
-
- def test_given_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldWithGivenMaxLengthModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
-
- def test_given_serializer_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
-
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
-
-
-class SlugFieldTests(TestCase):
- """
- Tests for SlugField attribute values
- """
-
- class SlugFieldModel(RESTFrameworkModel):
- slug_field = models.SlugField(blank=True)
-
- class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- slug_field = models.SlugField(max_length=84, blank=True)
-
- def test_default_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
-
- def test_given_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldWithGivenMaxLengthModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
-
- def test_given_serializer_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field',
- max_length=20, required=False)
-
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'],
- 'max_length'), 20)
-
- def test_invalid_slug(self):
- """
- Make sure an invalid slug raises ValidationError
- """
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
-
- class Meta:
- model = self.SlugFieldModel
-
- s = SlugFieldSerializer(data={'slug_field': 'a b'})
-
- self.assertEqual(s.is_valid(), False)
- self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
-
-
-class URLFieldTests(TestCase):
- """
- Tests for URLField attribute values
- """
-
- class URLFieldModel(RESTFrameworkModel):
- url_field = models.URLField(blank=True)
-
- class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- url_field = models.URLField(max_length=128, blank=True)
-
- def test_default_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 200)
-
- def test_given_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 128)
-
- def test_given_serializer_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- url_field = serializers.URLField(source='url_field',
- max_length=20, required=False)
-
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 20)
-
-
-class FieldMetadata(TestCase):
- def setUp(self):
- self.required_field = serializers.Field()
- self.required_field.label = uuid4().hex
- self.required_field.required = True
-
- self.optional_field = serializers.Field()
- self.optional_field.label = uuid4().hex
- self.optional_field.required = False
-
- def test_required(self):
- self.assertEqual(self.required_field.metadata()['required'], True)
-
- def test_optional(self):
- self.assertEqual(self.optional_field.metadata()['required'], False)
-
- def test_label(self):
- for field in (self.required_field, self.optional_field):
- self.assertEqual(field.metadata()['label'], field.label)
-
-
-class FieldCallableDefault(TestCase):
- def setUp(self):
- self.simple_callable = lambda: 'foo bar'
-
- def test_default_can_be_simple_callable(self):
- """
- Ensure that the 'default' argument can also be a simple callable.
- """
- field = serializers.WritableField(default=self.simple_callable)
- into = {}
- field.field_from_native({}, {}, 'field', into)
- self.assertEqual(into, {'field': 'foo bar'})
-
-
-class CustomIntegerField(TestCase):
- """
- Test that custom fields apply min_value and max_value constraints
- """
- def test_custom_fields_can_be_validated_for_value(self):
-
- class MoneyField(models.PositiveIntegerField):
- pass
-
- class EntryModel(models.Model):
- bank = MoneyField(validators=[validators.MaxValueValidator(100)])
-
- class EntrySerializer(serializers.ModelSerializer):
- class Meta:
- model = EntryModel
-
- entry = EntryModel(bank=1)
-
- serializer = EntrySerializer(entry, data={"bank": 11})
- self.assertTrue(serializer.is_valid())
-
- serializer = EntrySerializer(entry, data={"bank": -1})
- self.assertFalse(serializer.is_valid())
-
- serializer = EntrySerializer(entry, data={"bank": 101})
- self.assertFalse(serializer.is_valid())
-
-
diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py
deleted file mode 100644
index 487046aca..000000000
--- a/rest_framework/tests/test_files.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.compat import BytesIO
-from rest_framework.compat import six
-import datetime
-
-
-class UploadedFile(object):
- def __init__(self, file, created=None):
- self.file = file
- self.created = created or datetime.datetime.now()
-
-
-class UploadedFileSerializer(serializers.Serializer):
- file = serializers.FileField()
- created = serializers.DateTimeField()
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.file = attrs['file']
- instance.created = attrs['created']
- return instance
- return UploadedFile(**attrs)
-
-
-class FileSerializerTests(TestCase):
- def test_create(self):
- now = datetime.datetime.now()
- file = BytesIO(six.b('stuff'))
- file.name = 'stuff.txt'
- file.size = len(file.getvalue())
- serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
- uploaded_file = UploadedFile(file=file, created=now)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.object.created, uploaded_file.created)
- self.assertEqual(serializer.object.file, uploaded_file.file)
- self.assertFalse(serializer.object is uploaded_file)
-
- def test_creation_failure(self):
- """
- Passing files=None should result in an ValidationError
-
- Regression test for:
- https://github.com/tomchristie/django-rest-framework/issues/542
- """
- now = datetime.datetime.now()
-
- serializer = UploadedFileSerializer(data={'created': now})
- self.assertFalse(serializer.is_valid())
- self.assertIn('file', serializer.errors)
diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py
deleted file mode 100644
index c9d9e7ffa..000000000
--- a/rest_framework/tests/test_filters.py
+++ /dev/null
@@ -1,474 +0,0 @@
-from __future__ import unicode_literals
-import datetime
-from decimal import Decimal
-from django.db import models
-from django.core.urlresolvers import reverse
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import generics, serializers, status, filters
-from rest_framework.compat import django_filters, patterns, url
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel
-
-factory = APIRequestFactory()
-
-
-class FilterableItem(models.Model):
- text = models.CharField(max_length=100)
- decimal = models.DecimalField(max_digits=4, decimal_places=2)
- date = models.DateField()
-
-
-if django_filters:
- # Basic filter on a list view.
- class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_fields = ['decimal', 'date']
- filter_backends = (filters.DjangoFilterBackend,)
-
- # These class are used to test a filter class.
- class SeveralFieldsFilter(django_filters.FilterSet):
- text = django_filters.CharFilter(lookup_type='icontains')
- decimal = django_filters.NumberFilter(lookup_type='lt')
- date = django_filters.DateFilter(lookup_type='gt')
-
- class Meta:
- model = FilterableItem
- fields = ['text', 'decimal', 'date']
-
- class FilterClassRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- # These classes are used to test a misconfigured filter class.
- class MisconfiguredFilter(django_filters.FilterSet):
- text = django_filters.CharFilter(lookup_type='icontains')
-
- class Meta:
- model = BasicModel
- fields = ['text']
-
- class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_class = MisconfiguredFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- class FilterClassDetailView(generics.RetrieveAPIView):
- model = FilterableItem
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- # Regression test for #814
- class FilterableItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = FilterableItem
-
- class FilterFieldsQuerysetView(generics.ListCreateAPIView):
- queryset = FilterableItem.objects.all()
- serializer_class = FilterableItemSerializer
- filter_fields = ['decimal', 'date']
- filter_backends = (filters.DjangoFilterBackend,)
-
- class GetQuerysetView(generics.ListCreateAPIView):
- serializer_class = FilterableItemSerializer
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- def get_queryset(self):
- return FilterableItem.objects.all()
-
- urlpatterns = patterns('',
- url(r'^(?P\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
- url(r'^$', FilterClassRootView.as_view(), name='root-view'),
- url(r'^get-queryset/$', GetQuerysetView.as_view(),
- name='get-queryset-view'),
- )
-
-
-class CommonFilteringTestCase(TestCase):
- def _serialize_object(self, obj):
- return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
-
- def setUp(self):
- """
- Create 10 FilterableItem instances.
- """
- base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
- for i in range(10):
- text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
- decimal = base_data[1] + i
- date = base_data[2] - datetime.timedelta(days=i * 2)
- FilterableItem(text=text, decimal=decimal, date=date).save()
-
- self.objects = FilterableItem.objects
- self.data = [
- self._serialize_object(obj)
- for obj in self.objects.all()
- ]
-
-
-class IntegrationTestFiltering(CommonFilteringTestCase):
- """
- Integration tests for filtered list views.
- """
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_fields_root_view(self):
- """
- GET requests to paginated ListCreateAPIView should return paginated results.
- """
- view = FilterFieldsRootView.as_view()
-
- # Basic test with no filter.
- request = factory.get('/')
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- # Tests that the decimal filter works.
- search_decimal = Decimal('2.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the date filter works.
- search_date = datetime.date(2012, 9, 22)
- request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] == search_date]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_filter_with_queryset(self):
- """
- Regression test for #814.
- """
- view = FilterFieldsQuerysetView.as_view()
-
- # Tests that the decimal filter works.
- search_decimal = Decimal('2.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_filter_with_get_queryset_only(self):
- """
- Regression test for #834.
- """
- view = GetQuerysetView.as_view()
- request = factory.get('/get-queryset/')
- view(request).render()
- # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
- # here when neither `model' nor `queryset' was specified.
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_class_root_view(self):
- """
- GET requests to filtered ListCreateAPIView that have a filter_class set
- should return filtered results.
- """
- view = FilterClassRootView.as_view()
-
- # Basic test with no filter.
- request = factory.get('/')
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- # Tests that the decimal filter set with 'lt' in the filter class works.
- search_decimal = Decimal('4.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] < search_decimal]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the date filter set with 'gt' in the filter class works.
- search_date = datetime.date(2012, 10, 2)
- request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the text filter set with 'icontains' in the filter class works.
- search_text = 'ff'
- request = factory.get('/?text=%s' % search_text)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if search_text in f['text'].lower()]
- self.assertEqual(response.data, expected_data)
-
- # Tests that multiple filters works.
- search_decimal = Decimal('5.25')
- search_date = datetime.date(2012, 10, 2)
- request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date and
- f['decimal'] < search_decimal]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_incorrectly_configured_filter(self):
- """
- An error should be displayed when the filter class is misconfigured.
- """
- view = IncorrectlyConfiguredRootView.as_view()
-
- request = factory.get('/')
- self.assertRaises(AssertionError, view, request)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_unknown_filter(self):
- """
- GET requests with filters that aren't configured should return 200.
- """
- view = FilterFieldsRootView.as_view()
-
- search_integer = 10
- request = factory.get('/?integer=%s' % search_integer)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-class IntegrationTestDetailFiltering(CommonFilteringTestCase):
- """
- Integration tests for filtered detail views.
- """
- urls = 'rest_framework.tests.test_filters'
-
- def _get_url(self, item):
- return reverse('detail-view', kwargs=dict(pk=item.pk))
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_detail_view(self):
- """
- GET requests to filtered RetrieveAPIView that have a filter_class set
- should return filtered results.
- """
- item = self.objects.all()[0]
- data = self._serialize_object(item)
-
- # Basic test with no filter.
- response = self.client.get(self._get_url(item))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, data)
-
- # Tests that the decimal filter set that should fail.
- search_decimal = Decimal('4.25')
- high_item = self.objects.filter(decimal__gt=search_decimal)[0]
- response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal))
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- # Tests that the decimal filter set that should succeed.
- search_decimal = Decimal('4.25')
- low_item = self.objects.filter(decimal__lt=search_decimal)[0]
- low_item_data = self._serialize_object(low_item)
- response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, low_item_data)
-
- # Tests that multiple filters works.
- search_decimal = Decimal('5.25')
- search_date = datetime.date(2012, 10, 2)
- valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
- valid_item_data = self._serialize_object(valid_item)
- response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, valid_item_data)
-
-
-class SearchFilterModel(models.Model):
- title = models.CharField(max_length=20)
- text = models.CharField(max_length=100)
-
-
-class SearchFilterTests(TestCase):
- def setUp(self):
- # Sequence of title/text is:
- #
- # z abc
- # zz bcd
- # zzz cde
- # ...
- for idx in range(10):
- title = 'z' * (idx + 1)
- text = (
- chr(idx + ord('a')) +
- chr(idx + ord('b')) +
- chr(idx + ord('c'))
- )
- SearchFilterModel(title=title, text=text).save()
-
- def test_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('title', 'text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=b')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 1, 'title': 'z', 'text': 'abc'},
- {'id': 2, 'title': 'zz', 'text': 'bcd'}
- ]
- )
-
- def test_exact_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('=title', 'text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=zzz')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'zzz', 'text': 'cde'}
- ]
- )
-
- def test_startswith_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('title', '^text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=b')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 2, 'title': 'zz', 'text': 'bcd'}
- ]
- )
-
-
-class OrdringFilterModel(models.Model):
- title = models.CharField(max_length=20)
- text = models.CharField(max_length=100)
-
-
-class OrderingFilterTests(TestCase):
- def setUp(self):
- # Sequence of title/text is:
- #
- # zyx abc
- # yxw bcd
- # xwv cde
- for idx in range(3):
- title = (
- chr(ord('z') - idx) +
- chr(ord('y') - idx) +
- chr(ord('x') - idx)
- )
- text = (
- chr(idx + ord('a')) +
- chr(idx + ord('b')) +
- chr(idx + ord('c'))
- )
- OrdringFilterModel(title=title, text=text).save()
-
- def test_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=text')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- ]
- )
-
- def test_reverse_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=-text')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_incorrectfield_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=foobar')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_default_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_default_ordering_using_string(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = 'title'
-
- view = OrderingListView.as_view()
- request = factory.get('')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py
deleted file mode 100644
index 61e613d75..000000000
--- a/rest_framework/tests/test_hyperlinkedserializers.py
+++ /dev/null
@@ -1,333 +0,0 @@
-from __future__ import unicode_literals
-import json
-from django.test import TestCase
-from rest_framework import generics, status, serializers
-from rest_framework.compat import patterns, url
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import (
- Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
- Album, Photo, OptionalRelationModel
-)
-
-factory = APIRequestFactory()
-
-
-class BlogPostCommentSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
- text = serializers.CharField()
- blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
-
- class Meta:
- model = BlogPostComment
- fields = ('text', 'blog_post_url', 'url')
-
-
-class PhotoSerializer(serializers.Serializer):
- description = serializers.CharField()
- album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title')
-
- def restore_object(self, attrs, instance=None):
- return Photo(**attrs)
-
-
-class AlbumSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
-
- class Meta:
- model = Album
- fields = ('title', 'url')
-
-
-class BasicList(generics.ListCreateAPIView):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class AnchorDetail(generics.RetrieveAPIView):
- model = Anchor
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class ManyToManyList(generics.ListAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class ManyToManyDetail(generics.RetrieveAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class BlogPostCommentListCreate(generics.ListCreateAPIView):
- model = BlogPostComment
- serializer_class = BlogPostCommentSerializer
-
-
-class BlogPostCommentDetail(generics.RetrieveAPIView):
- model = BlogPostComment
- serializer_class = BlogPostCommentSerializer
-
-
-class BlogPostDetail(generics.RetrieveAPIView):
- model = BlogPost
-
-
-class PhotoListCreate(generics.ListCreateAPIView):
- model = Photo
- model_serializer_class = PhotoSerializer
-
-
-class AlbumDetail(generics.RetrieveAPIView):
- model = Album
- serializer_class = AlbumSerializer
- lookup_field = 'title'
-
-
-class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
- model = OptionalRelationModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-urlpatterns = patterns('',
- url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
- url(r'^basic/(?P\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
- url(r'^anchor/(?P\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
- url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
- url(r'^manytomany/(?P\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
- url(r'^posts/(?P\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
- url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
- url(r'^comments/(?P\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
- url(r'^albums/(?P\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
- url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
- url(r'^optionalrelation/(?P\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
-)
-
-
-class TestBasicHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- for item in items:
- BasicModel(text=item).save()
- self.objects = BasicModel.objects
- self.data = [
- {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.list_view = BasicList.as_view()
- self.detail_view = BasicDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/')
- response = self.list_view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/1')
- response = self.detail_view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestManyToManyHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- anchors = []
- for item in items:
- anchor = Anchor(text=item)
- anchor.save()
- anchors.append(anchor)
-
- manytomany = ManyToManyModel()
- manytomany.save()
- manytomany.rel.add(*anchors)
-
- self.data = [{
- 'url': 'http://testserver/manytomany/1/',
- 'rel': [
- 'http://testserver/anchor/1/',
- 'http://testserver/anchor/2/',
- 'http://testserver/anchor/3/',
- ]
- }]
- self.list_view = ManyToManyList.as_view()
- self.detail_view = ManyToManyDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/')
- response = self.list_view(request)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/1/')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestHyperlinkedIdentityFieldLookup(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 Album instances.
- """
- titles = ['foo', 'bar', 'baz']
- for title in titles:
- album = Album(title=title)
- album.save()
- self.detail_view = AlbumDetail.as_view()
- self.data = {
- 'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
- 'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
- 'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
- }
-
- def test_lookup_field(self):
- """
- GET requests to AlbumDetail view should return serialized Albums
- with a url field keyed by `title`.
- """
- for album in Album.objects.all():
- request = factory.get('/albums/{0}/'.format(album.title))
- response = self.detail_view(request, title=album.title)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[album.title])
-
-
-class TestCreateWithForeignKeys(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create a blog post
- """
- self.post = BlogPost.objects.create(title="Test post")
- self.create_view = BlogPostCommentListCreate.as_view()
-
- def test_create_comment(self):
-
- data = {
- 'text': 'A test comment',
- 'blog_post_url': 'http://testserver/posts/1/'
- }
-
- request = factory.post('/comments/', data=data)
- response = self.create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response['Location'], 'http://testserver/comments/1/')
- self.assertEqual(self.post.blogpostcomment_set.count(), 1)
- self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
-
-
-class TestCreateWithForeignKeysAndCustomSlug(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create an Album
- """
- self.post = Album.objects.create(title='test-album')
- self.list_create_view = PhotoListCreate.as_view()
-
- def test_create_photo(self):
-
- data = {
- 'description': 'A test photo',
- 'album_url': 'http://testserver/albums/test-album/'
- }
-
- request = factory.post('/photos/', data=data)
- response = self.list_create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
- self.assertEqual(self.post.photo_set.count(), 1)
- self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
-
-
-class TestOptionalRelationHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 1 OptionalRelationModel instances.
- """
- OptionalRelationModel().save()
- self.objects = OptionalRelationModel.objects
- self.detail_view = OptionalRelationDetail.as_view()
- self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
-
- def test_get_detail_view(self):
- """
- GET requests to RetrieveAPIView with optional relations should return None
- for non existing relations.
- """
- request = factory.get('/optionalrelationmodel-detail/1')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_put_detail_view(self):
- """
- PUT requests to RetrieveUpdateDestroyAPIView with optional relations
- should accept None for non existing relations.
- """
- response = self.client.put('/optionalrelation/1/',
- data=json.dumps(self.data),
- content_type='application/json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-class TestOverriddenURLField(TestCase):
- def setUp(self):
- class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
- url = serializers.SerializerMethodField('get_url')
-
- class Meta:
- model = BlogPost
- fields = ('title', 'url')
-
- def get_url(self, obj):
- return 'foo bar'
-
- self.Serializer = OverriddenURLSerializer
- self.obj = BlogPost.objects.create(title='New blog post')
-
- def test_overridden_url_field(self):
- """
- The 'url' field should respect overriding.
- Regression test for #936.
- """
- serializer = self.Serializer(self.obj)
- self.assertEqual(
- serializer.data,
- {'title': 'New blog post', 'url': 'foo bar'}
- )
diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py
deleted file mode 100644
index 85d4640ea..000000000
--- a/rest_framework/tests/test_pagination.py
+++ /dev/null
@@ -1,385 +0,0 @@
-from __future__ import unicode_literals
-import datetime
-from decimal import Decimal
-from django.db import models
-from django.core.paginator import Paginator
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import generics, status, pagination, filters, serializers
-from rest_framework.compat import django_filters
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel
-
-factory = APIRequestFactory()
-
-
-class FilterableItem(models.Model):
- text = models.CharField(max_length=100)
- decimal = models.DecimalField(max_digits=4, decimal_places=2)
- date = models.DateField()
-
-
-class RootView(generics.ListCreateAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
- paginate_by = 10
-
-
-class DefaultPageSizeKwargView(generics.ListAPIView):
- """
- View for testing default paginate_by_param usage
- """
- model = BasicModel
-
-
-class PaginateByParamView(generics.ListAPIView):
- """
- View for testing custom paginate_by_param usage
- """
- model = BasicModel
- paginate_by_param = 'page_size'
-
-
-class IntegrationTestPagination(TestCase):
- """
- Integration tests for paginated list views.
- """
-
- def setUp(self):
- """
- Create 26 BasicModel instances.
- """
- for char in 'abcdefghijklmnopqrstuvwxyz':
- BasicModel(text=char * 3).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = RootView.as_view()
-
- def test_get_paginated_root_view(self):
- """
- GET requests to paginated ListCreateAPIView should return paginated results.
- """
- request = factory.get('/')
- # Note: Database queries are a `SELECT COUNT`, and `SELECT `
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[10:20])
- self.assertNotEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[20:])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
-
-class IntegrationTestPaginationAndFiltering(TestCase):
-
- def setUp(self):
- """
- Create 50 FilterableItem instances.
- """
- base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
- for i in range(26):
- text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
- decimal = base_data[1] + i
- date = base_data[2] - datetime.timedelta(days=i * 2)
- FilterableItem(text=text, decimal=decimal, date=date).save()
-
- self.objects = FilterableItem.objects
- self.data = [
- {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
- for obj in self.objects.all()
- ]
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_django_filter_paginated_filtered_root_view(self):
- """
- GET requests to paginated filtered ListCreateAPIView should return
- paginated results. The next and previous links should preserve the
- filtered parameters.
- """
- class DecimalFilter(django_filters.FilterSet):
- decimal = django_filters.NumberFilter(lookup_type='lt')
-
- class Meta:
- model = FilterableItem
- fields = ['text', 'decimal', 'date']
-
- class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- paginate_by = 10
- filter_class = DecimalFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- view = FilterFieldsRootView.as_view()
-
- EXPECTED_NUM_QUERIES = 2
-
- request = factory.get('/?decimal=15.20')
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[10:15])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['previous'])
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- def test_get_basic_paginated_filtered_root_view(self):
- """
- Same as `test_get_django_filter_paginated_filtered_root_view`,
- except using a custom filter backend instead of the django-filter
- backend,
- """
-
- class DecimalFilterBackend(filters.BaseFilterBackend):
- def filter_queryset(self, request, queryset, view):
- return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
-
- class BasicFilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- paginate_by = 10
- filter_backends = (DecimalFilterBackend,)
-
- view = BasicFilterFieldsRootView.as_view()
-
- request = factory.get('/?decimal=15.20')
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[10:15])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['previous'])
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
-
-class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
- class Meta:
- object_serializer_class = serializers.Serializer
-
-
-class UnitTestPagination(TestCase):
- """
- Unit tests for pagination of primitive objects.
- """
-
- def setUp(self):
- self.objects = [char * 3 for char in 'abcdefghijklmnopqrstuvwxyz']
- paginator = Paginator(self.objects, 10)
- self.first_page = paginator.page(1)
- self.last_page = paginator.page(3)
-
- def test_native_pagination(self):
- serializer = pagination.PaginationSerializer(self.first_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], '?page=2')
- self.assertEqual(serializer.data['previous'], None)
- self.assertEqual(serializer.data['results'], self.objects[:10])
-
- serializer = pagination.PaginationSerializer(self.last_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], None)
- self.assertEqual(serializer.data['previous'], '?page=2')
- self.assertEqual(serializer.data['results'], self.objects[20:])
-
- def test_context_available_in_result(self):
- """
- Ensure context gets passed through to the object serializer.
- """
- serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'})
- serializer.data
- results = serializer.fields[serializer.results_field]
- self.assertEqual(serializer.context, results.context)
-
-
-class TestUnpaginated(TestCase):
- """
- Tests for list views without pagination.
- """
-
- def setUp(self):
- """
- Create 13 BasicModel instances.
- """
- for i in range(13):
- BasicModel(text=i).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = DefaultPageSizeKwargView.as_view()
-
- def test_unpaginated(self):
- """
- Tests the default page size for this view.
- no page size --> no limit --> no meta data
- """
- request = factory.get('/')
- response = self.view(request)
- self.assertEqual(response.data, self.data)
-
-
-class TestCustomPaginateByParam(TestCase):
- """
- Tests for list views with default page size kwarg
- """
-
- def setUp(self):
- """
- Create 13 BasicModel instances.
- """
- for i in range(13):
- BasicModel(text=i).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = PaginateByParamView.as_view()
-
- def test_default_page_size(self):
- """
- Tests the default page size for this view.
- no page size --> no limit --> no meta data
- """
- request = factory.get('/')
- response = self.view(request).render()
- self.assertEqual(response.data, self.data)
-
- def test_paginate_by_param(self):
- """
- If paginate_by_param is set, the new kwarg should limit per view requests.
- """
- request = factory.get('/?page_size=5')
- response = self.view(request).render()
- self.assertEqual(response.data['count'], 13)
- self.assertEqual(response.data['results'], self.data[:5])
-
-
-### Tests for context in pagination serializers
-
-class CustomField(serializers.Field):
- def to_native(self, value):
- if not 'view' in self.context:
- raise RuntimeError("context isn't getting passed into custom field")
- return "value"
-
-
-class BasicModelSerializer(serializers.Serializer):
- text = CustomField()
-
- def __init__(self, *args, **kwargs):
- super(BasicModelSerializer, self).__init__(*args, **kwargs)
- if not 'view' in self.context:
- raise RuntimeError("context isn't getting passed into serializer init")
-
-
-class TestContextPassedToCustomField(TestCase):
- def setUp(self):
- BasicModel.objects.create(text='ala ma kota')
-
- def test_with_pagination(self):
- class ListView(generics.ListCreateAPIView):
- model = BasicModel
- serializer_class = BasicModelSerializer
- paginate_by = 1
-
- self.view = ListView.as_view()
- request = factory.get('/')
- response = self.view(request).render()
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-### Tests for custom pagination serializers
-
-class LinksSerializer(serializers.Serializer):
- next = pagination.NextPageField(source='*')
- prev = pagination.PreviousPageField(source='*')
-
-
-class CustomPaginationSerializer(pagination.BasePaginationSerializer):
- links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.Field(source='paginator.count')
-
- results_field = 'objects'
-
-
-class TestCustomPaginationSerializer(TestCase):
- def setUp(self):
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = Paginator(objects, 2)
- self.page = paginator.page(1)
-
- def test_custom_pagination_serializer(self):
- request = APIRequestFactory().get('/foobar')
- serializer = CustomPaginationSerializer(
- instance=self.page,
- context={'request': request}
- )
- expected = {
- 'links': {
- 'next': 'http://testserver/foobar?page=2',
- 'prev': None
- },
- 'total_results': 4,
- 'objects': ['john', 'paul']
- }
- self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_parsers.py b/rest_framework/tests/test_parsers.py
deleted file mode 100644
index 7699e10c9..000000000
--- a/rest_framework/tests/test_parsers.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from __future__ import unicode_literals
-from rest_framework.compat import StringIO
-from django import forms
-from django.core.files.uploadhandler import MemoryFileUploadHandler
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework.compat import etree
-from rest_framework.parsers import FormParser, FileUploadParser
-from rest_framework.parsers import XMLParser
-import datetime
-
-
-class Form(forms.Form):
- field1 = forms.CharField(max_length=3)
- field2 = forms.CharField()
-
-
-class TestFormParser(TestCase):
- def setUp(self):
- self.string = "field1=abc&field2=defghijk"
-
- def test_parse(self):
- """ Make sure the `QueryDict` works OK """
- parser = FormParser()
-
- stream = StringIO(self.string)
- data = parser.parse(stream)
-
- self.assertEqual(Form(data).is_valid(), True)
-
-
-class TestXMLParser(TestCase):
- def setUp(self):
- self._input = StringIO(
- ''
- ''
- '121.0 '
- 'dasd '
- ' '
- '2011-12-25 12:45:00 '
- ' '
- )
- self._data = {
- 'field_a': 121,
- 'field_b': 'dasd',
- 'field_c': None,
- 'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }
- self._complex_data_input = StringIO(
- ''
- ''
- '2011-12-25 12:45:00 '
- ''
- '1 first '
- '2 second '
- ' '
- 'name '
- ' '
- )
- self._complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_parse(self):
- parser = XMLParser()
- data = parser.parse(self._input)
- self.assertEqual(data, self._data)
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_complex_data_parse(self):
- parser = XMLParser()
- data = parser.parse(self._complex_data_input)
- self.assertEqual(data, self._complex_data)
-
-
-class TestFileUploadParser(TestCase):
- def setUp(self):
- class MockRequest(object):
- pass
- from io import BytesIO
- self.stream = BytesIO(
- "Test text file".encode('utf-8')
- )
- request = MockRequest()
- request.upload_handlers = (MemoryFileUploadHandler(),)
- request.META = {
- 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'),
- 'HTTP_CONTENT_LENGTH': 14,
- }
- self.parser_context = {'request': request, 'kwargs': {}}
-
- def test_parse(self):
- """ Make sure the `QueryDict` works OK """
- parser = FileUploadParser()
- self.stream.seek(0)
- data_and_files = parser.parse(self.stream, None, self.parser_context)
- file_obj = data_and_files.files['file']
- self.assertEqual(file_obj._size, 14)
-
- def test_get_filename(self):
- parser = FileUploadParser()
- filename = parser.get_filename(self.stream, None, self.parser_context)
- self.assertEqual(filename, 'file.txt'.encode('utf-8'))
diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py
deleted file mode 100644
index e2cca3808..000000000
--- a/rest_framework/tests/test_permissions.py
+++ /dev/null
@@ -1,188 +0,0 @@
-from __future__ import unicode_literals
-from django.contrib.auth.models import User, Permission
-from django.db import models
-from django.test import TestCase
-from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
-from rest_framework.test import APIRequestFactory
-import base64
-
-factory = APIRequestFactory()
-
-
-class BasicModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class RootView(generics.ListCreateAPIView):
- model = BasicModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [permissions.DjangoModelPermissions]
-
-
-class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [permissions.DjangoModelPermissions]
-
-root_view = RootView.as_view()
-instance_view = InstanceView.as_view()
-
-
-def basic_auth_header(username, password):
- credentials = ('%s:%s' % (username, password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- return 'Basic %s' % base64_credentials
-
-
-class ModelPermissionsIntegrationTests(TestCase):
- def setUp(self):
- User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
- user = User.objects.create_user('permitted', 'permitted@example.com', 'password')
- user.user_permissions = [
- Permission.objects.get(codename='add_basicmodel'),
- Permission.objects.get(codename='change_basicmodel'),
- Permission.objects.get(codename='delete_basicmodel')
- ]
- user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
- user.user_permissions = [
- Permission.objects.get(codename='change_basicmodel'),
- ]
-
- self.permitted_credentials = basic_auth_header('permitted', 'password')
- self.disallowed_credentials = basic_auth_header('disallowed', 'password')
- self.updateonly_credentials = basic_auth_header('updateonly', 'password')
-
- BasicModel(text='foo').save()
-
- def test_has_create_permissions(self):
- request = factory.post('/', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = root_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- def test_has_put_permissions(self):
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_has_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- def test_does_not_have_create_permissions(self):
- request = factory.post('/', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = root_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_does_not_have_put_permissions(self):
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_does_not_have_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_has_put_as_create_permissions(self):
- # User only has update permissions - should be able to update an entity.
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # But if PUTing to a new entity, permission should be denied.
- request = factory.put('/2', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='2')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_options_permitted(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['POST'])
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
-
- def test_options_disallowed(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- def test_options_updateonly(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
-
-
-class OwnerModel(models.Model):
- text = models.CharField(max_length=100)
- owner = models.ForeignKey(User)
-
-
-class IsOwnerPermission(permissions.BasePermission):
- def has_object_permission(self, request, view, obj):
- return request.user == obj.owner
-
-
-class OwnerInstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = OwnerModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [IsOwnerPermission]
-
-
-owner_instance_view = OwnerInstanceView.as_view()
-
-
-class ObjectPermissionsIntegrationTests(TestCase):
- """
- Integration tests for the object level permissions API.
- """
-
- def setUp(self):
- User.objects.create_user('not_owner', 'not_owner@example.com', 'password')
- user = User.objects.create_user('owner', 'owner@example.com', 'password')
-
- self.not_owner_credentials = basic_auth_header('not_owner', 'password')
- self.owner_credentials = basic_auth_header('owner', 'password')
-
- OwnerModel(text='foo', owner=user).save()
-
- def test_owner_has_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.owner_credentials)
- response = owner_instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- def test_non_owner_does_not_have_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.not_owner_credentials)
- response = owner_instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py
deleted file mode 100644
index d19219c90..000000000
--- a/rest_framework/tests/test_relations.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""
-General tests for relational fields.
-"""
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.tests.models import BlogPost
-
-
-class NullModel(models.Model):
- pass
-
-
-class FieldTests(TestCase):
- def test_pk_related_field_with_empty_string(self):
- """
- Regression test for #446
-
- https://github.com/tomchristie/django-rest-framework/issues/446
- """
- field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_hyperlinked_related_field_with_empty_string(self):
- field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_slug_related_field_with_empty_string(self):
- field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
-
-class TestManyRelatedMixin(TestCase):
- def test_missing_many_to_many_related_field(self):
- '''
- Regression test for #632
-
- https://github.com/tomchristie/django-rest-framework/pull/632
- '''
- field = serializers.RelatedField(many=True, read_only=False)
-
- into = {}
- field.field_from_native({}, None, 'field_name', into)
- self.assertEqual(into['field_name'], [])
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class RelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py
deleted file mode 100644
index f6d006b39..000000000
--- a/rest_framework/tests/test_relations_nested.py
+++ /dev/null
@@ -1,107 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
-
-
-class ForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeySource
- fields = ('id', 'name', 'target')
- depth = 1
-
-
-class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeyTarget
- fields = ('id', 'name', 'sources')
- depth = 1
-
-
-class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = NullableForeignKeySource
- fields = ('id', 'name', 'target')
- depth = 1
-
-
-class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneTarget
- fields = ('id', 'name', 'nullable_source')
- depth = 1
-
-
-class ReverseForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- new_target = ForeignKeyTarget(name='target-2')
- new_target.save()
- for idx in range(1, 4):
- source = ForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve(self):
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 3, 'name': 'source-3', 'target': {'id': 1, 'name': 'target-1'}},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_retrieve(self):
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': 1},
- ]},
- {'id': 2, 'name': 'target-2', 'sources': [
- ]}
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class NestedNullableForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- if idx == 3:
- target = None
- source = NullableForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve_with_null(self):
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 3, 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class NestedNullableOneToOneTests(TestCase):
- def setUp(self):
- target = OneToOneTarget(name='target-1')
- target.save()
- new_target = OneToOneTarget(name='target-2')
- new_target.save()
- source = NullableOneToOneSource(name='source-1', target=target)
- source.save()
-
- def test_reverse_foreign_key_retrieve_with_null(self):
- queryset = OneToOneTarget.objects.all()
- serializer = NullableOneToOneTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}},
- {'id': 2, 'name': 'target-2', 'nullable_source': None},
- ]
- self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py
deleted file mode 100644
index 5fcccb741..000000000
--- a/rest_framework/tests/test_routers.py
+++ /dev/null
@@ -1,216 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from django.core.exceptions import ImproperlyConfigured
-from rest_framework import serializers, viewsets, permissions
-from rest_framework.compat import include, patterns, url
-from rest_framework.decorators import link, action
-from rest_framework.response import Response
-from rest_framework.routers import SimpleRouter, DefaultRouter
-from rest_framework.test import APIRequestFactory
-
-factory = APIRequestFactory()
-
-urlpatterns = patterns('',)
-
-
-class BasicViewSet(viewsets.ViewSet):
- def list(self, request, *args, **kwargs):
- return Response({'method': 'list'})
-
- @action()
- def action1(self, request, *args, **kwargs):
- return Response({'method': 'action1'})
-
- @action()
- def action2(self, request, *args, **kwargs):
- return Response({'method': 'action2'})
-
- @action(methods=['post', 'delete'])
- def action3(self, request, *args, **kwargs):
- return Response({'method': 'action2'})
-
- @link()
- def link1(self, request, *args, **kwargs):
- return Response({'method': 'link1'})
-
- @link()
- def link2(self, request, *args, **kwargs):
- return Response({'method': 'link2'})
-
-
-class TestSimpleRouter(TestCase):
- def setUp(self):
- self.router = SimpleRouter()
-
- def test_link_and_action_decorator(self):
- routes = self.router.get_routes(BasicViewSet)
- decorator_routes = routes[2:]
- # Make sure all these endpoints exist and none have been clobbered
- for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
- route = decorator_routes[i]
- # check url listing
- self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
- # check method to function mapping
- if endpoint == 'action3':
- methods_map = ['post', 'delete']
- elif endpoint.startswith('action'):
- methods_map = ['post']
- else:
- methods_map = ['get']
- for method in methods_map:
- self.assertEqual(route.mapping[method], endpoint)
-
-
-class RouterTestModel(models.Model):
- uuid = models.CharField(max_length=20)
- text = models.CharField(max_length=200)
-
-
-class TestCustomLookupFields(TestCase):
- """
- Ensure that custom lookup fields are correctly routed.
- """
- urls = 'rest_framework.tests.test_routers'
-
- def setUp(self):
- class NoteSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = RouterTestModel
- lookup_field = 'uuid'
- fields = ('url', 'uuid', 'text')
-
- class NoteViewSet(viewsets.ModelViewSet):
- queryset = RouterTestModel.objects.all()
- serializer_class = NoteSerializer
- lookup_field = 'uuid'
-
- RouterTestModel.objects.create(uuid='123', text='foo bar')
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
-
- from rest_framework.tests import test_routers
- urls = getattr(test_routers, 'urlpatterns')
- urls += patterns('',
- url(r'^', include(self.router.urls)),
- )
-
- def test_custom_lookup_field_route(self):
- detail_route = self.router.urls[-1]
- detail_url_pattern = detail_route.regex.pattern
- self.assertIn('', detail_url_pattern)
-
- def test_retrieve_lookup_field_list_view(self):
- response = self.client.get('/notes/')
- self.assertEqual(response.data,
- [{
- "url": "http://testserver/notes/123/",
- "uuid": "123", "text": "foo bar"
- }]
- )
-
- def test_retrieve_lookup_field_detail_view(self):
- response = self.client.get('/notes/123/')
- self.assertEqual(response.data,
- {
- "url": "http://testserver/notes/123/",
- "uuid": "123", "text": "foo bar"
- }
- )
-
-
-class TestTrailingSlashIncluded(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_urls_have_trailing_slash_by_default(self):
- expected = ['^notes/$', '^notes/(?P[^/]+)/$']
- for idx in range(len(expected)):
- self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
-
-
-class TestTrailingSlashRemoved(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
-
- self.router = SimpleRouter(trailing_slash=False)
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_urls_can_have_trailing_slash_removed(self):
- expected = ['^notes$', '^notes/(?P[^/]+)$']
- for idx in range(len(expected)):
- self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
-
-
-class TestNameableRoot(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
- self.router = DefaultRouter()
- self.router.root_view_name = 'nameable-root'
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_router_has_custom_name(self):
- expected = 'nameable-root'
- self.assertEqual(expected, self.urls[0].name)
-
-
-class TestActionKeywordArgs(TestCase):
- """
- Ensure keyword arguments passed in the `@action` decorator
- are properly handled. Refs #940.
- """
-
- def setUp(self):
- class TestViewSet(viewsets.ModelViewSet):
- permission_classes = []
-
- @action(permission_classes=[permissions.AllowAny])
- def custom(self, request, *args, **kwargs):
- return Response({
- 'permission_classes': self.permission_classes
- })
-
- self.router = SimpleRouter()
- self.router.register(r'test', TestViewSet, base_name='test')
- self.view = self.router.urls[-1].callback
-
- def test_action_kwargs(self):
- request = factory.post('/test/0/custom/')
- response = self.view(request)
- self.assertEqual(
- response.data,
- {'permission_classes': [permissions.AllowAny]}
- )
-
-
-class TestActionAppliedToExistingRoute(TestCase):
- """
- Ensure `@action` decorator raises an except when applied
- to an existing route
- """
-
- def test_exception_raised_when_action_applied_to_existing_route(self):
- class TestViewSet(viewsets.ModelViewSet):
-
- @action()
- def retrieve(self, request, *args, **kwargs):
- return Response({
- 'hello': 'world'
- })
-
- self.router = SimpleRouter()
- self.router.register(r'test', TestViewSet, base_name='test')
-
- with self.assertRaises(ImproperlyConfigured):
- self.router.urls
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
deleted file mode 100644
index c24976603..000000000
--- a/rest_framework/tests/test_serializer.py
+++ /dev/null
@@ -1,1645 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.test import TestCase
-from django.utils.datastructures import MultiValueDict
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers, fields, relations
-from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
- BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
-from rest_framework.tests.models import BasicModelSerializer
-import datetime
-import pickle
-
-
-class SubComment(object):
- def __init__(self, sub_comment):
- self.sub_comment = sub_comment
-
-
-class Comment(object):
- def __init__(self, email, content, created):
- self.email = email
- self.content = content
- self.created = created or datetime.datetime.now()
-
- def __eq__(self, other):
- return all([getattr(self, attr) == getattr(other, attr)
- for attr in ('email', 'content', 'created')])
-
- def get_sub_comment(self):
- sub_comment = SubComment('And Merry Christmas!')
- return sub_comment
-
-
-class CommentSerializer(serializers.Serializer):
- email = serializers.EmailField()
- content = serializers.CharField(max_length=1000)
- created = serializers.DateTimeField()
- sub_comment = serializers.Field(source='get_sub_comment.sub_comment')
-
- def restore_object(self, data, instance=None):
- if instance is None:
- return Comment(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
-
-
-class NamesSerializer(serializers.Serializer):
- first = serializers.CharField()
- last = serializers.CharField(required=False, default='')
- initials = serializers.CharField(required=False, default='')
-
-
-class PersonIdentifierSerializer(serializers.Serializer):
- ssn = serializers.CharField()
- names = NamesSerializer(source='names', required=False)
-
-
-class BookSerializer(serializers.ModelSerializer):
- isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
-
- class Meta:
- model = Book
-
-
-class ActionItemSerializer(serializers.ModelSerializer):
-
- class Meta:
- model = ActionItem
-
-
-class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
-
- class Meta:
- model = ActionItem
-
- def restore_object(self, data, instance=None):
- if instance is None:
- return ActionItem(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
-
-
-class PersonSerializer(serializers.ModelSerializer):
- info = serializers.Field(source='info')
-
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age',)
-
-
-class NestedSerializer(serializers.Serializer):
- info = serializers.Field()
-
-
-class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
- nested = NestedSerializer(source='*')
-
- class Meta:
- model = Person
-
-
-class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
- """
- Testing for #652.
- """
- info = serializers.Field(source='info')
-
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age', 'info')
-
-
-class AlbumsSerializer(serializers.ModelSerializer):
-
- class Meta:
- model = Album
- fields = ['title'] # lists are also valid options
-
-
-class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
- class Meta:
- model = HasPositiveIntegerAsChoice
- fields = ['some_integer']
-
-
-class BasicTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'This wont change'
- }
- self.expected = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'And Merry Christmas!'
- }
- self.person_data = {'name': 'dwight', 'age': 35}
- self.person = Person(**self.person_data)
- self.person.save()
-
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': '',
- 'content': '',
- 'created': None,
- 'sub_comment': ''
- }
- self.assertEqual(serializer.data, expected)
-
- def test_retrieve(self):
- serializer = CommentSerializer(self.comment)
- self.assertEqual(serializer.data, self.expected)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertFalse(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_create_nested(self):
- """Test a serializer with nested data."""
- names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- self.assertEqual(serializer.data['names'], names)
-
- def test_create_partial_nested(self):
- """Test a serializer with nested data which has missing fields."""
- names = {'first': 'John'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- expected_names = {'first': 'John', 'last': '', 'initials': ''}
- data['names'] = expected_names
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is expected_names)
- self.assertEqual(serializer.data['names'], expected_names)
-
- def test_null_nested(self):
- """Test a serializer with a nonexistent nested field"""
- data = {'ssn': '1234567890'}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- expected = {'ssn': '1234567890', 'names': None}
- self.assertEqual(serializer.data, expected)
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_partial_update(self):
- msg = 'Merry New Year!'
- partial_data = {'content': msg}
- serializer = CommentSerializer(self.comment, data=partial_data)
- self.assertEqual(serializer.is_valid(), False)
- serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['content'], msg)
-
- def test_model_fields_as_expected(self):
- """
- Make sure that the fields returned are the same as defined
- in the Meta data
- """
- serializer = PersonSerializer(self.person)
- self.assertEqual(set(serializer.data.keys()),
- set(['name', 'age', 'info']))
-
- def test_field_with_dictionary(self):
- """
- Make sure that dictionaries from fields are left intact
- """
- serializer = PersonSerializer(self.person)
- expected = self.person_data
- self.assertEqual(serializer.data['info'], expected)
-
- def test_read_only_fields(self):
- """
- Attempting to update fields set as read_only should have no effect.
- """
- serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(serializer.errors, {})
- # Assert age is unchanged (35)
- self.assertEqual(instance.age, self.person_data['age'])
-
- def test_invalid_read_only_fields(self):
- """
- Regression test for #652.
- """
- self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
-
-
-class DictStyleSerializer(serializers.Serializer):
- """
- Note that we don't have any `restore_object` method, so the default
- case of simply returning a dict will apply.
- """
- email = serializers.EmailField()
-
-
-class DictStyleSerializerTests(TestCase):
- def test_dict_style_deserialize(self):
- """
- Ensure serializers can deserialize into a dict.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
-
- def test_dict_style_serialize(self):
- """
- Ensure serializers can serialize dict objects.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data)
- self.assertEqual(serializer.data, data)
-
-
-class ValidationTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'x' * 1001,
- 'created': datetime.datetime(2012, 1, 1)
- }
- self.actionitem = ActionItem(title='Some to do item',)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update_missing_field(self):
- data = {
- 'content': 'xxx',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = CommentSerializer(self.comment, data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'email': ['This field is required.']})
-
- def test_missing_bool_with_default(self):
- """Make sure that a boolean value with a 'False' value is not
- mistaken for not having a default."""
- data = {
- 'title': 'Some action item',
- #No 'done' value.
- }
- serializer = ActionItemSerializer(self.actionitem, data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_cross_field_validation(self):
-
- class CommentSerializerWithCrossFieldValidator(CommentSerializer):
-
- def validate(self, attrs):
- if attrs["email"] not in attrs["content"]:
- raise serializers.ValidationError("Email address not in content")
- return attrs
-
- data = {
- 'email': 'tom@example.com',
- 'content': 'A comment from tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'A comment from foo@bar.com'
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'non_field_errors': ['Email address not in content']})
-
- def test_null_is_true_fields(self):
- """
- Omitting a value for null-field should validate.
- """
- serializer = PersonSerializer(data={'name': 'marko'})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_modelserializer_max_length_exceeded(self):
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_modelserializer_max_length_exceeded_with_custom_restore(self):
- """
- When overriding ModelSerializer.restore_object, validation tests should still apply.
- Regression test for #623.
-
- https://github.com/tomchristie/django-rest-framework/pull/623
- """
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializerCustomRestore(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_default_modelfield_max_length_exceeded(self):
- data = {
- 'title': 'Testing "info" field...',
- 'info': 'x' * 13,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
-
- def test_datetime_validation_failure(self):
- """
- Test DateTimeField validation errors on non-str values.
- Regression test for #669.
-
- https://github.com/tomchristie/django-rest-framework/issues/669
- """
- data = self.data
- data['created'] = 0
-
- serializer = CommentSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
-
- self.assertIn('created', serializer.errors)
-
- def test_missing_model_field_exception_msg(self):
- """
- Assert that a meaningful exception message is outputted when the model
- field is missing (e.g. when mistyping ``model``).
- """
- class BrokenModelSerializer(serializers.ModelSerializer):
- class Meta:
- fields = ['some_field']
-
- try:
- BrokenModelSerializer()
- except AssertionError as e:
- self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
- except:
- self.fail('Wrong exception type thrown.')
-
- def test_writable_star_source_on_nested_serializer(self):
- """
- Assert that a nested serializer instantiated with source='*' correctly
- expands the data into the outer serializer.
- """
- serializer = ModelSerializerWithNestedSerializer(data={
- 'name': 'marko',
- 'nested': {'info': 'hi'}},
- )
- self.assertEqual(serializer.is_valid(), True)
-
-
-class CustomValidationTests(TestCase):
- class CommentSerializerWithFieldValidator(CommentSerializer):
-
- def validate_email(self, attrs, source):
- attrs[source]
- return attrs
-
- def validate_content(self, attrs, source):
- value = attrs[source]
- if "test" not in value:
- raise serializers.ValidationError("Test not in value")
- return attrs
-
- def test_field_validation(self):
- data = {
- 'email': 'tom@example.com',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'This should not validate'
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['Test not in value']})
-
- def test_missing_data(self):
- """
- Make sure that validate_content isn't called if the field is missing
- """
- incomplete_data = {
- 'email': 'tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['This field is required.']})
-
- def test_wrong_data(self):
- """
- Make sure that validate_content isn't called if the field input is wrong
- """
- wrong_data = {
- 'email': 'not an email',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
-
-
-class PositiveIntegerAsChoiceTests(TestCase):
- def test_positive_integer_in_json_is_correctly_parsed(self):
- data = {'some_integer': 1}
- serializer = PositiveIntegerAsChoiceSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
-
-
-class ModelValidationTests(TestCase):
- def test_validate_unique(self):
- """
- Just check if serializers.ModelSerializer handles unique checks via .full_clean()
- """
- serializer = AlbumsSerializer(data={'title': 'a'})
- serializer.is_valid()
- serializer.save()
- second_serializer = AlbumsSerializer(data={'title': 'a'})
- self.assertFalse(second_serializer.is_valid())
- self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
-
- def test_foreign_key_with_partial(self):
- """
- Test ModelSerializer validation with partial=True
-
- Specifically test foreign key validation.
- """
-
- album = Album(title='test')
- album.save()
-
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
-
- photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
- self.assertTrue(photo_serializer.is_valid())
- photo = photo_serializer.save()
-
- # Updating only the album (foreign key)
- photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
- # Updating only the description
- photo_serializer = PhotoSerializer(instance=photo,
- data={'description': 'new'},
- partial=True)
-
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
-
-class RegexValidationTest(TestCase):
- def test_create_failed(self):
- serializer = BookSerializer(data={'isbn': '1234567890'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': '12345678901234'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': 'abcdefghijklm'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- def test_create_success(self):
- serializer = BookSerializer(data={'isbn': '1234567890123'})
- self.assertTrue(serializer.is_valid())
-
-
-class MetadataTests(TestCase):
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': serializers.CharField,
- 'content': serializers.CharField,
- 'created': serializers.DateTimeField
- }
- for field_name, field in expected.items():
- self.assertTrue(isinstance(serializer.data.fields[field_name], field))
-
-
-class ManyToManyTests(TestCase):
- def setUp(self):
- class ManyToManySerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyModel
-
- self.serializer_class = ManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'id': 1, 'rel': [self.anchor.id]}
-
- def test_retrieve(self):
- """
- Serialize an instance of a model with a ManyToMany relationship.
- """
- serializer = self.serializer_class(instance=self.instance)
- expected = self.data
- self.assertEqual(serializer.data, expected)
-
- def test_create(self):
- """
- Create an instance of a model with a ManyToMany relationship.
- """
- data = {'rel': [self.anchor.id]}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update(self):
- """
- Update an instance of a model with a ManyToMany relationship.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [self.anchor, new_anchor])
-
- def test_create_empty_relationship(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- data = {'rel': []}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_update_empty_relationship(self):
- """
- Update an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': []}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_create_empty_relationship_flat_data(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items, using a representation that does not support
- lists (eg form data).
- """
- data = MultiValueDict()
- data.setlist('rel', [''])
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
-
-class ReadOnlyManyToManyTests(TestCase):
- def setUp(self):
- class ReadOnlyManyToManySerializer(serializers.ModelSerializer):
- rel = serializers.RelatedField(many=True, read_only=True)
-
- class Meta:
- model = ReadOnlyManyToManyModel
-
- self.serializer_class = ReadOnlyManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ReadOnlyManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'}
-
- def test_update(self):
- """
- Attempt to update an instance of a model with a ManyToMany
- relationship. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update_without_relationship(self):
- """
- Attempt to update an instance of a model where many to ManyToMany
- relationship is not supplied. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
-
-class DefaultValueTests(TestCase):
- def setUp(self):
- class DefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = DefaultValueModel
-
- self.serializer_class = DefaultValueSerializer
- self.objects = DefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
- def test_partial_update_default(self):
- """ Regression test for issue #532 """
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- data = {'extra': 'extra_value'}
- serializer = self.serializer_class(instance=instance, data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- self.assertEqual(instance.extra, 'extra_value')
- self.assertEqual(instance.text, 'overridden')
-
-
-class CallableDefaultValueTests(TestCase):
- def setUp(self):
- class CallableDefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = CallableDefaultValueModel
-
- self.serializer_class = CallableDefaultValueSerializer
- self.objects = CallableDefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
-
-class ManyRelatedTests(TestCase):
- def test_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'title': 'Test blog post',
- 'comments': [
- {'text': 'I hate this blog post'},
- {'text': 'I love this blog post'}
- ]
- }
-
- self.assertEqual(serializer.data, expected)
-
- def test_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post', 'blogpostcomment_set': [1, 2]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_depth_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
- depth = 1
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post',
- 'blogpostcomment_set': [
- {'id': 1, 'text': 'I hate this blog post', 'blog_post': 1},
- {'id': 2, 'text': 'I love this blog post', 'blog_post': 1}
- ]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_callable_source(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- first_comment = BlogPostCommentSerializer(source='get_first_comment')
-
- serializer = BlogPostSerializer(post)
-
- expected = {
- 'title': 'Test blog post',
- 'first_comment': {'text': 'I love this blog post'}
- }
- self.assertEqual(serializer.data, expected)
-
-
-class RelatedTraversalTest(TestCase):
- def test_nested_traversal(self):
- """
- Source argument should support dotted.source notation.
- """
- user = Person.objects.create(name="django")
- post = BlogPost.objects.create(title="Test blog post", writer=user)
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ("name", "age")
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- fields = ("text", "post_owner")
-
- text = serializers.CharField()
- post_owner = PersonSerializer(source='blog_post.writer')
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
-
- expected = {
- 'title': 'Test blog post',
- 'comments': [{
- 'text': 'I love this blog post',
- 'post_owner': {
- "name": "django",
- "age": None
- }
- }]
- }
-
- self.assertEqual(serializer.data, expected)
-
- def test_nested_traversal_with_none(self):
- """
- If a component of the dotted.source is None, return None for the field.
- """
- from rest_framework.tests.models import NullableForeignKeySource
- instance = NullableForeignKeySource.objects.create(name='Source with null FK')
-
- class NullableSourceSerializer(serializers.Serializer):
- target_name = serializers.Field(source='target.name')
-
- serializer = NullableSourceSerializer(instance=instance)
-
- expected = {
- 'target_name': None,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-class SerializerMethodFieldTests(TestCase):
- def setUp(self):
-
- class BoopSerializer(serializers.Serializer):
- beep = serializers.SerializerMethodField('get_beep')
- boop = serializers.Field()
- boop_count = serializers.SerializerMethodField('get_boop_count')
-
- def get_beep(self, obj):
- return 'hello!'
-
- def get_boop_count(self, obj):
- return len(obj.boop)
-
- self.serializer_class = BoopSerializer
-
- def test_serializer_method_field(self):
-
- class MyModel(object):
- boop = ['a', 'b', 'c']
-
- source_data = MyModel()
-
- serializer = self.serializer_class(source_data)
-
- expected = {
- 'beep': 'hello!',
- 'boop': ['a', 'b', 'c'],
- 'boop_count': 3,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-# Test for issue #324
-class BlankFieldTests(TestCase):
- def setUp(self):
-
- class BlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlankFieldModel
-
- class BlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField(required=False)
-
- class NotBlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
-
- class NotBlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField()
-
- self.model_serializer_class = BlankFieldModelSerializer
- self.serializer_class = BlankFieldSerializer
- self.not_blank_model_serializer_class = NotBlankFieldModelSerializer
- self.not_blank_serializer_class = NotBlankFieldSerializer
- self.data = {'title': ''}
-
- def test_create_blank_field(self):
- serializer = self.serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_blank_field(self):
- serializer = self.model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_null_field(self):
- serializer = self.model_serializer_class(data={'title': None})
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a non-model serializer
- """
- serializer = self.not_blank_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a model serializer
- """
- serializer = self.not_blank_model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_empty_field(self):
- serializer = self.model_serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
-
-#test for issue #460
-class SerializerPickleTests(TestCase):
- """
- Test pickleability of the output of Serializers
- """
- def test_pickle_simple_model_serializer_data(self):
- """
- Test simple serializer
- """
- pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
-
- def test_pickle_inner_serializer(self):
- """
- Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
- have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
- See DictWithMetadata.__getstate__
- """
- class InnerPersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ('name', 'age')
- pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data, 0)
-
- def test_getstate_method_should_not_return_none(self):
- """
- Regression test for #645.
- """
- data = serializers.DictWithMetadata({1: 1})
- self.assertEqual(data.__getstate__(), serializers.SortedDict({1: 1}))
-
- def test_serializer_data_is_pickleable(self):
- """
- Another regression test for #645.
- """
- data = serializers.SortedDictWithMetadata({1: 1})
- repr(pickle.loads(pickle.dumps(data, 0)))
-
-
-# test for issue #725
-class SeveralChoicesModel(models.Model):
- color = models.CharField(
- max_length=10,
- choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
- blank=False
- )
- drink = models.CharField(
- max_length=10,
- choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
- blank=False,
- default='beer'
- )
- os = models.CharField(
- max_length=10,
- choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
- blank=True
- )
- music_genre = models.CharField(
- max_length=10,
- choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
- blank=True,
- default='metal'
- )
-
-
-class SerializerChoiceFields(TestCase):
-
- def setUp(self):
- super(SerializerChoiceFields, self).setUp()
-
- class SeveralChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = SeveralChoicesModel
- fields = ('color', 'drink', 'os', 'music_genre')
-
- self.several_choices_serializer = SeveralChoicesSerializer
-
- def test_choices_blank_false_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['color'].choices,
- [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
- )
-
- def test_choices_blank_false_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['drink'].choices,
- [('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
- )
-
- def test_choices_blank_true_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['os'].choices,
- BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
- )
-
- def test_choices_blank_true_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['music_genre'].choices,
- BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
- )
-
-
-# Regression tests for #675
-class Ticket(models.Model):
- assigned = models.ForeignKey(
- Person, related_name='assigned_tickets')
- reviewer = models.ForeignKey(
- Person, blank=True, null=True, related_name='reviewed_tickets')
-
-
-class SerializerRelatedChoicesTest(TestCase):
-
- def setUp(self):
- super(SerializerRelatedChoicesTest, self).setUp()
-
- class RelatedChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = Ticket
- fields = ('assigned', 'reviewer')
-
- self.related_fields_serializer = RelatedChoicesSerializer
-
- def test_empty_queryset_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- []
- )
-
- def test_empty_queryset_not_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------')]
- )
-
- def test_with_some_persons_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- [(1, 'Person object - 1'), (2, 'Person object - 2')]
- )
-
- def test_with_some_persons_not_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
- )
-
-
-class DepthTest(TestCase):
- def test_implicit_nesting(self):
-
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- depth = 2
-
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
-
- self.assertEqual(serializer.data, expected)
-
- def test_explicit_nesting(self):
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
-
- class BlogPostSerializer(serializers.ModelSerializer):
- writer = PersonSerializer()
-
- class Meta:
- model = BlogPost
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- blog_post = BlogPostSerializer()
-
- class Meta:
- model = BlogPostComment
-
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
-
- self.assertEqual(serializer.data, expected)
-
-
-class NestedSerializerContextTests(TestCase):
-
- def test_nested_serializer_context(self):
- """
- Regression for #497
-
- https://github.com/tomchristie/django-rest-framework/issues/497
- """
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
- fields = ("description", "callable")
-
- callable = serializers.SerializerMethodField('_callable')
-
- def _callable(self, instance):
- if not 'context_item' in self.context:
- raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
- return "success"
-
- class AlbumSerializer(serializers.ModelSerializer):
- class Meta:
- model = Album
- fields = ("photo_set", "callable")
-
- photo_set = PhotoSerializer(source="photo_set")
- callable = serializers.SerializerMethodField("_callable")
-
- def _callable(self, instance):
- if not 'context_item' in self.context:
- raise RuntimeError("context isn't getting passed into 1st level nested serializer")
- return "success"
-
- class AlbumCollection(object):
- albums = None
-
- class AlbumCollectionSerializer(serializers.Serializer):
- albums = AlbumSerializer(source="albums")
-
- album1 = Album.objects.create(title="album 1")
- album2 = Album.objects.create(title="album 2")
- Photo.objects.create(description="Bigfoot", album=album1)
- Photo.objects.create(description="Unicorn", album=album1)
- Photo.objects.create(description="Yeti", album=album2)
- Photo.objects.create(description="Sasquatch", album=album2)
- album_collection = AlbumCollection()
- album_collection.albums = [album1, album2]
-
- # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
- AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
-
-
-class DeserializeListTestCase(TestCase):
-
- def setUp(self):
- self.data = {
- 'email': 'nobody@nowhere.com',
- 'content': 'This is some test content',
- 'created': datetime.datetime(2013, 3, 7),
- }
-
- def test_no_errors(self):
- data = [self.data.copy() for x in range(0, 3)]
- serializer = CommentSerializer(data=data, many=True)
- self.assertTrue(serializer.is_valid())
- self.assertTrue(isinstance(serializer.object, list))
- self.assertTrue(
- all((isinstance(item, Comment) for item in serializer.object))
- )
-
- def test_errors_return_as_list(self):
- invalid_item = self.data.copy()
- invalid_item['email'] = ''
- data = [self.data.copy(), invalid_item, self.data.copy()]
-
- serializer = CommentSerializer(data=data, many=True)
- self.assertFalse(serializer.is_valid())
- expected = [{}, {'email': ['This field is required.']}, {}]
- self.assertEqual(serializer.errors, expected)
-
-
-# Test for issue 747
-
-class LazyStringModel(object):
- def __init__(self, lazystring):
- self.lazystring = lazystring
-
-
-class LazyStringSerializer(serializers.Serializer):
- lazystring = serializers.Field()
-
- def restore_object(self, attrs, instance=None):
- if instance is not None:
- instance.lazystring = attrs.get('lazystring', instance.lazystring)
- return instance
- return LazyStringModel(**attrs)
-
-
-class LazyStringsTestCase(TestCase):
- def setUp(self):
- self.model = LazyStringModel(lazystring=_('lazystring'))
-
- def test_lazy_strings_are_translated(self):
- serializer = LazyStringSerializer(self.model)
- self.assertEqual(type(serializer.data['lazystring']),
- type('lazystring'))
-
-
-# Test for issue #467
-
-class FieldLabelTest(TestCase):
- def setUp(self):
- self.serializer_class = BasicModelSerializer
-
- def test_label_from_model(self):
- """
- Validates that label and help_text are correctly copied from the model class.
- """
- serializer = self.serializer_class()
- text_field = serializer.fields['text']
-
- self.assertEqual('Text comes here', text_field.label)
- self.assertEqual('Text description.', text_field.help_text)
-
- def test_field_ctor(self):
- """
- This is check that ctor supports both label and help_text.
- """
- self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label)
- self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text)
- self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
-
-
-# Test for issue #961
-
-class ManyFieldHelpTextTest(TestCase):
- def test_help_text_no_hold_down_control_msg(self):
- """
- Validate that help_text doesn't contain the 'Hold down "Control" ...'
- message that Django appends to choice fields.
- """
- rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
- self.assertEqual('Some help text.', rel_field.help_text)
-
-
-class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
- class AMOAFModel(RESTFrameworkModel):
- char_field = models.CharField(max_length=1024, blank=True)
- comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
- decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
- email_field = models.EmailField(max_length=1024, blank=True)
- file_field = models.FileField(max_length=1024, blank=True)
- image_field = models.ImageField(max_length=1024, blank=True)
- slug_field = models.SlugField(max_length=1024, blank=True)
- url_field = models.URLField(max_length=1024, blank=True)
-
- class AMOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = AMOAFModel
-
- self.serializer_class = AMOAFSerializer
- self.fields_attributes = {
- 'char_field': [
- ('max_length', 1024),
- ],
- 'comma_separated_integer_field': [
- ('max_length', 1024),
- ],
- 'decimal_field': [
- ('max_digits', 64),
- ('decimal_places', 32),
- ],
- 'email_field': [
- ('max_length', 1024),
- ],
- 'file_field': [
- ('max_length', 1024),
- ],
- 'image_field': [
- ('max_length', 1024),
- ],
- 'slug_field': [
- ('max_length', 1024),
- ],
- 'url_field': [
- ('max_length', 1024),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
-
- def test_char_field(self):
- self.field_test('char_field')
-
- def test_comma_separated_integer_field(self):
- self.field_test('comma_separated_integer_field')
-
- def test_decimal_field(self):
- self.field_test('decimal_field')
-
- def test_email_field(self):
- self.field_test('email_field')
-
- def test_file_field(self):
- self.field_test('file_field')
-
- def test_image_field(self):
- self.field_test('image_field')
-
- def test_slug_field(self):
- self.field_test('slug_field')
-
- def test_url_field(self):
- self.field_test('url_field')
-
-
-class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
- class DVOAFModel(RESTFrameworkModel):
- positive_integer_field = models.PositiveIntegerField(blank=True)
- positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
- email_field = models.EmailField(blank=True)
- file_field = models.FileField(blank=True)
- image_field = models.ImageField(blank=True)
- slug_field = models.SlugField(blank=True)
- url_field = models.URLField(blank=True)
-
- class DVOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = DVOAFModel
-
- self.serializer_class = DVOAFSerializer
- self.fields_attributes = {
- 'positive_integer_field': [
- ('min_value', 0),
- ],
- 'positive_small_integer_field': [
- ('min_value', 0),
- ],
- 'email_field': [
- ('max_length', 75),
- ],
- 'file_field': [
- ('max_length', 100),
- ],
- 'image_field': [
- ('max_length', 100),
- ],
- 'slug_field': [
- ('max_length', 50),
- ],
- 'url_field': [
- ('max_length', 200),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
-
- def test_positive_integer_field(self):
- self.field_test('positive_integer_field')
-
- def test_positive_small_integer_field(self):
- self.field_test('positive_small_integer_field')
-
- def test_email_field(self):
- self.field_test('email_field')
-
- def test_file_field(self):
- self.field_test('file_field')
-
- def test_image_field(self):
- self.field_test('image_field')
-
- def test_slug_field(self):
- self.field_test('slug_field')
-
- def test_url_field(self):
- self.field_test('url_field')
-
-
-class MetadataSerializer(serializers.Serializer):
- field1 = serializers.CharField(3, required=True)
- field2 = serializers.CharField(10, required=False)
-
-
-class MetadataSerializerTestCase(TestCase):
- def setUp(self):
- self.serializer = MetadataSerializer()
-
- def test_serializer_metadata(self):
- metadata = self.serializer.metadata()
- expected = {
- 'field1': {
- 'required': True,
- 'max_length': 3,
- 'type': 'string',
- 'read_only': False
- },
- 'field2': {
- 'required': False,
- 'max_length': 10,
- 'type': 'string',
- 'read_only': False
- }
- }
- self.assertEqual(expected, metadata)
-
-
-### Regression test for #840
-
-class SimpleModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class SimpleModelSerializer(serializers.ModelSerializer):
- text = serializers.CharField()
- other = serializers.CharField()
-
- class Meta:
- model = SimpleModel
-
- def validate_other(self, attrs, source):
- del attrs['other']
- return attrs
-
-
-class FieldValidationRemovingAttr(TestCase):
- def test_removing_non_model_field_in_validation(self):
- """
- Removing an attr during field valiation should ensure that it is not
- passed through when restoring the object.
-
- This allows additional non-model fields to be supported.
-
- Regression test for #840.
- """
- serializer = SimpleModelSerializer(data={'text': 'foo', 'other': 'bar'})
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.object.text, 'foo')
-
-
-### Regression test for #878
-
-class SimpleTargetModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class SimplePKSourceModelSerializer(serializers.Serializer):
- targets = serializers.PrimaryKeyRelatedField(queryset=SimpleTargetModel.objects.all(), many=True)
- text = serializers.CharField()
-
-
-class SimpleSlugSourceModelSerializer(serializers.Serializer):
- targets = serializers.SlugRelatedField(queryset=SimpleTargetModel.objects.all(), many=True, slug_field='pk')
- text = serializers.CharField()
-
-
-class SerializerSupportsManyRelationships(TestCase):
- def setUp(self):
- SimpleTargetModel.objects.create(text='foo')
- SimpleTargetModel.objects.create(text='bar')
-
- def test_serializer_supports_pk_many_relationships(self):
- """
- Regression test for #878.
-
- Note that pk behavior has a different code path to usual cases,
- for performance reasons.
- """
- serializer = SimplePKSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
-
- def test_serializer_supports_slug_many_relationships(self):
- """
- Regression test for #878.
- """
- serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
diff --git a/rest_framework/tests/test_serializer_bulk_update.py b/rest_framework/tests/test_serializer_bulk_update.py
deleted file mode 100644
index 8b0ded1a8..000000000
--- a/rest_framework/tests/test_serializer_bulk_update.py
+++ /dev/null
@@ -1,278 +0,0 @@
-"""
-Tests to cover bulk create and update using serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-
-
-class BulkCreateSerializerTests(TestCase):
- """
- Creating multiple instances using serializers.
- """
-
- def setUp(self):
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- self.BookSerializer = BookSerializer
-
- def test_bulk_create_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_bulk_create_errors(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 'foo',
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {},
- {'id': ['Enter a whole number.']}
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_list_datatype(self):
- """
- Data containing list of incorrect data type should return errors.
- """
- data = ['foo', 'bar', 'baz']
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = [
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']}
- ]
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_datatype(self):
- """
- Data containing a single incorrect data type should return errors.
- """
- data = 123
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_object(self):
- """
- Data containing only a single object, instead of a list of objects
- should return errors.
- """
- data = {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class BulkUpdateSerializerTests(TestCase):
- """
- Updating multiple instances using serializers.
- """
-
- def setUp(self):
- class Book(object):
- """
- A data type that can be persisted to a mock storage backend
- with `.save()` and `.delete()`.
- """
- object_map = {}
-
- def __init__(self, id, title, author):
- self.id = id
- self.title = title
- self.author = author
-
- def save(self):
- Book.object_map[self.id] = self
-
- def delete(self):
- del Book.object_map[self.id]
-
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.id = attrs['id']
- instance.title = attrs['title']
- instance.author = attrs['author']
- return instance
- return Book(**attrs)
-
- self.Book = Book
- self.BookSerializer = BookSerializer
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- for item in data:
- book = Book(item['id'], item['title'], item['author'])
- book.save()
-
- def books(self):
- """
- Return all the objects in the mock storage backend.
- """
- return self.Book.object_map.values()
-
- def test_bulk_update_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 2,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
-
- self.assertEqual(data, new_data)
-
- def test_bulk_update_and_create(self):
- """
- Bulk update serialization may also include created items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
- self.assertEqual(data, new_data)
-
- def test_bulk_update_invalid_create(self):
- """
- Bulk update serialization without allow_add_remove may not create items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_bulk_update_error(self):
- """
- Incorrect bulk update serialization should return error data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 'foo',
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'id': ['Enter a whole number.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
deleted file mode 100644
index 71d0e24b5..000000000
--- a/rest_framework/tests/test_serializer_nested.py
+++ /dev/null
@@ -1,246 +0,0 @@
-"""
-Tests to cover nested serializers.
-
-Doesn't cover model serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-
-
-class WritableNestedSerializerBasicTests(TestCase):
- """
- Tests for deserializing nested entities.
- Basic tests that use serializers that simply restore to dicts.
- """
-
- def setUp(self):
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- expected_errors = {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_many_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data
- when multiple entities are being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- ]
- expected_errors = [
- {},
- {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class WritableNestedSerializerObjectTests(TestCase):
- """
- Tests for deserializing nested entities.
- These tests use serializers that restore to concrete objects.
- """
-
- def setUp(self):
- # Couple of concrete objects that we're going to deserialize into
- class Track(object):
- def __init__(self, order, title, duration):
- self.order, self.title, self.duration = order, title, duration
-
- def __eq__(self, other):
- return (
- self.order == other.order and
- self.title == other.title and
- self.duration == other.duration
- )
-
- class Album(object):
- def __init__(self, album_name, artist, tracks):
- self.album_name, self.artist, self.tracks = album_name, artist, tracks
-
- def __eq__(self, other):
- return (
- self.album_name == other.album_name and
- self.artist == other.artist and
- self.tracks == other.tracks
- )
-
- # And their corresponding serializers
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- def restore_object(self, attrs, instance=None):
- return Track(attrs['order'], attrs['title'], attrs['duration'])
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- def restore_object(self, attrs, instance=None):
- return Album(attrs['album_name'], attrs['artist'], attrs['tracks'])
-
- self.Album, self.Track = Album, Track
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return a restored object
- that corresponds to the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- expected_object = self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
-
- def test_many_nested_validation_success(self):
- """
- Correct nested serialization should return multiple restored objects
- that corresponds to the input data when multiple objects are
- being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- ]
- expected_object = [
- self.Album(
- album_name='Russian Red',
- artist='I Love Your Glasses',
- tracks=[
- self.Track(order=1, title='Cigarettes', duration=121),
- self.Track(order=2, title='No Past Land', duration=198),
- self.Track(order=3, title='They Don\'t Believe', duration=191),
- ]
- ),
- self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
diff --git a/rest_framework/tests/test_settings.py b/rest_framework/tests/test_settings.py
deleted file mode 100644
index 857375c21..000000000
--- a/rest_framework/tests/test_settings.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Tests for the settings module"""
-from __future__ import unicode_literals
-from django.test import TestCase
-
-from rest_framework.settings import APISettings, DEFAULTS, IMPORT_STRINGS
-
-
-class TestSettings(TestCase):
- """Tests relating to the api settings"""
-
- def test_non_import_errors(self):
- """Make sure other errors aren't suppressed."""
- settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
- with self.assertRaises(ValueError):
- settings.DEFAULT_MODEL_SERIALIZER_CLASS
-
- def test_import_error_message_maintained(self):
- """Make sure real import errors are captured and raised sensibly."""
- settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
- with self.assertRaises(ImportError) as cm:
- settings.DEFAULT_MODEL_SERIALIZER_CLASS
- self.assertTrue('ImportError' in str(cm.exception))
diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py
deleted file mode 100644
index 49d45fc29..000000000
--- a/rest_framework/tests/test_testing.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# -- coding: utf-8 --
-
-from __future__ import unicode_literals
-from django.contrib.auth.models import User
-from django.test import TestCase
-from rest_framework.compat import patterns, url
-from rest_framework.decorators import api_view
-from rest_framework.response import Response
-from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
-
-
-@api_view(['GET', 'POST'])
-def view(request):
- return Response({
- 'auth': request.META.get('HTTP_AUTHORIZATION', b''),
- 'user': request.user.username
- })
-
-
-urlpatterns = patterns('',
- url(r'^view/$', view),
-)
-
-
-class TestAPITestClient(TestCase):
- urls = 'rest_framework.tests.test_testing'
-
- def setUp(self):
- self.client = APIClient()
-
- def test_credentials(self):
- """
- Setting `.credentials()` adds the required headers to each request.
- """
- self.client.credentials(HTTP_AUTHORIZATION='example')
- for _ in range(0, 3):
- response = self.client.get('/view/')
- self.assertEqual(response.data['auth'], 'example')
-
- def test_force_authenticate(self):
- """
- Setting `.force_authenticate()` forcibly authenticates each request.
- """
- user = User.objects.create_user('example', 'example@example.com')
- self.client.force_authenticate(user)
- response = self.client.get('/view/')
- self.assertEqual(response.data['user'], 'example')
-
- def test_csrf_exempt_by_default(self):
- """
- By default, the test client is CSRF exempt.
- """
- User.objects.create_user('example', 'example@example.com', 'password')
- self.client.login(username='example', password='password')
- response = self.client.post('/view/')
- self.assertEqual(response.status_code, 200)
-
- def test_explicitly_enforce_csrf_checks(self):
- """
- The test client can enforce CSRF checks.
- """
- client = APIClient(enforce_csrf_checks=True)
- User.objects.create_user('example', 'example@example.com', 'password')
- client.login(username='example', password='password')
- response = client.post('/view/')
- expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
- self.assertEqual(response.status_code, 403)
- self.assertEqual(response.data, expected)
-
-
-class TestAPIRequestFactory(TestCase):
- def test_csrf_exempt_by_default(self):
- """
- By default, the test client is CSRF exempt.
- """
- user = User.objects.create_user('example', 'example@example.com', 'password')
- factory = APIRequestFactory()
- request = factory.post('/view/')
- request.user = user
- response = view(request)
- self.assertEqual(response.status_code, 200)
-
- def test_explicitly_enforce_csrf_checks(self):
- """
- The test client can enforce CSRF checks.
- """
- user = User.objects.create_user('example', 'example@example.com', 'password')
- factory = APIRequestFactory(enforce_csrf_checks=True)
- request = factory.post('/view/')
- request.user = user
- response = view(request)
- expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
- self.assertEqual(response.status_code, 403)
- self.assertEqual(response.data, expected)
-
- def test_invalid_format(self):
- """
- Attempting to use a format that is not configured will raise an
- assertion error.
- """
- factory = APIRequestFactory()
- self.assertRaises(AssertionError, factory.post,
- path='/view/', data={'example': 1}, format='xml'
- )
-
- def test_force_authenticate(self):
- """
- Setting `force_authenticate()` forcibly authenticates the request.
- """
- user = User.objects.create_user('example', 'example@example.com')
- factory = APIRequestFactory()
- request = factory.get('/view')
- force_authenticate(request, user=user)
- response = view(request)
- self.assertEqual(response.data['user'], 'example')
diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py
deleted file mode 100644
index ebfdff9cd..000000000
--- a/rest_framework/tests/test_validation.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from rest_framework import generics, serializers, status
-from rest_framework.test import APIRequestFactory
-
-factory = APIRequestFactory()
-
-
-# Regression for #666
-
-class ValidationModel(models.Model):
- blank_validated_field = models.CharField(max_length=255)
-
-
-class ValidationModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = ValidationModel
- fields = ('blank_validated_field',)
- read_only_fields = ('blank_validated_field',)
-
-
-class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
- model = ValidationModel
- serializer_class = ValidationModelSerializer
-
-
-class TestPreSaveValidationExclusions(TestCase):
- def test_pre_save_validation_exclusions(self):
- """
- Somewhat weird test case to ensure that we don't perform model
- validation on read only fields.
- """
- obj = ValidationModel.objects.create(blank_validated_field='')
- request = factory.put('/', {}, format='json')
- view = UpdateValidationModel().as_view()
- response = view(request, pk=obj.pk).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-# Regression for #653
-
-class ShouldValidateModel(models.Model):
- should_validate_field = models.CharField(max_length=255)
-
-
-class ShouldValidateModelSerializer(serializers.ModelSerializer):
- renamed = serializers.CharField(source='should_validate_field', required=False)
-
- class Meta:
- model = ShouldValidateModel
- fields = ('renamed',)
-
-
-class TestPreSaveValidationExclusions(TestCase):
- def test_renamed_fields_are_model_validated(self):
- """
- Ensure fields with 'source' applied do get still get model validation.
- """
- # We've set `required=False` on the serializer, but the model
- # does not have `blank=True`, so this serializer should not validate.
- serializer = ShouldValidateModelSerializer(data={'renamed': ''})
- self.assertEqual(serializer.is_valid(), False)
-
-
-class ValidationSerializer(serializers.Serializer):
- foo = serializers.CharField()
-
- def validate_foo(self, attrs, source):
- raise serializers.ValidationError("foo invalid")
-
- def validate(self, attrs):
- raise serializers.ValidationError("serializer invalid")
-
-
-class TestAvoidValidation(TestCase):
- """
- If serializer was initialized with invalid data (None or non dict-like), it
- should avoid validation layer (validate_ and validate methods)
- """
- def test_serializer_errors_has_only_invalid_data_error(self):
- serializer = ValidationSerializer(data='invalid data')
- self.assertFalse(serializer.is_valid())
- self.assertDictEqual(serializer.errors,
- {'non_field_errors': ['Invalid data']})
diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py
deleted file mode 100644
index 554ebd1ad..000000000
--- a/rest_framework/tests/tests.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-Force import of all modules in this package in order to get the standard test
-runner to pick up the tests. Yowzers.
-"""
-from __future__ import unicode_literals
-import os
-import django
-
-modules = [filename.rsplit('.', 1)[0]
- for filename in os.listdir(os.path.dirname(__file__))
- if filename.endswith('.py') and not filename.startswith('_')]
-__test__ = dict()
-
-if django.VERSION < (1, 6):
- for module in modules:
- exec("from rest_framework.tests.%s import *" % module)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index f6bb1cc84..261fc2463 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -2,7 +2,7 @@
Provides various throttling policies.
"""
from __future__ import unicode_literals
-from django.core.cache import cache
+from django.core.cache import cache as default_cache
from django.core.exceptions import ImproperlyConfigured
from rest_framework.settings import api_settings
import time
@@ -18,6 +18,25 @@ class BaseThrottle(object):
"""
raise NotImplementedError('.allow_request() must be overridden')
+ def get_ident(self, request):
+ """
+ Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR
+ if present and number of proxies is > 0. If not use all of
+ HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR.
+ """
+ xff = request.META.get('HTTP_X_FORWARDED_FOR')
+ remote_addr = request.META.get('REMOTE_ADDR')
+ num_proxies = api_settings.NUM_PROXIES
+
+ if num_proxies is not None:
+ if num_proxies == 0 or xff is None:
+ return remote_addr
+ addrs = xff.split(',')
+ client_addr = addrs[-min(num_proxies, len(addrs))]
+ return client_addr.strip()
+
+ return ''.join(xff.split()) if xff else remote_addr
+
def wait(self):
"""
Optionally, return a recommended number of seconds to wait before
@@ -39,8 +58,9 @@ class SimpleRateThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache.
"""
+ cache = default_cache
timer = time.time
- cache_format = 'throtte_%(scope)s_%(ident)s'
+ cache_format = 'throttle_%(scope)s_%(ident)s'
scope = None
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
@@ -96,7 +116,10 @@ class SimpleRateThrottle(BaseThrottle):
return True
self.key = self.get_cache_key(request, view)
- self.history = cache.get(self.key, [])
+ if self.key is None:
+ return True
+
+ self.history = self.cache.get(self.key, [])
self.now = self.timer()
# Drop any requests from the history which have now passed the
@@ -113,7 +136,7 @@ class SimpleRateThrottle(BaseThrottle):
into the cache.
"""
self.history.insert(0, self.now)
- cache.set(self.key, self.history, self.duration)
+ self.cache.set(self.key, self.history, self.duration)
return True
def throttle_failure(self):
@@ -132,6 +155,8 @@ class SimpleRateThrottle(BaseThrottle):
remaining_duration = self.duration
available_requests = self.num_requests - len(self.history) + 1
+ if available_requests <= 0:
+ return None
return remaining_duration / float(available_requests)
@@ -148,11 +173,9 @@ class AnonRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated():
return None # Only throttle unauthenticated requests.
- ident = request.META.get('REMOTE_ADDR', None)
-
return self.cache_format % {
'scope': self.scope,
- 'ident': ident
+ 'ident': self.get_ident(request)
}
@@ -168,9 +191,9 @@ class UserRateThrottle(SimpleRateThrottle):
def get_cache_key(self, request, view):
if request.user.is_authenticated():
- ident = request.user.id
+ ident = request.user.pk
else:
- ident = request.META.get('REMOTE_ADDR', None)
+ ident = self.get_ident(request)
return self.cache_format % {
'scope': self.scope,
@@ -216,9 +239,9 @@ class ScopedRateThrottle(SimpleRateThrottle):
with the '.throttle_scope` property of the view.
"""
if request.user.is_authenticated():
- ident = request.user.id
+ ident = request.user.pk
else:
- ident = request.META.get('REMOTE_ADDR', None)
+ ident = self.get_ident(request)
return self.cache_format % {
'scope': self.scope,
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index d9143bb4c..038e9ee38 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
+from django.conf.urls import url, include
from django.core.urlresolvers import RegexURLResolver
-from rest_framework.compat import url, include
from rest_framework.settings import api_settings
@@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
allowed_pattern = '(%s)' % '|'.join(allowed)
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
else:
- suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
+ suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 9c4719f1d..cfcee534b 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -2,23 +2,25 @@
Login and logout views for the browsable API.
Add these to your root URLconf if you're using the browsable API and
-your API requires authentication.
-
-The urls must be namespaced as 'rest_framework', and you should make sure
-your authentication settings include `SessionAuthentication`.
+your API requires authentication:
urlpatterns = patterns('',
...
- url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
)
+
+The urls must be namespaced as 'rest_framework', and you should make sure
+your authentication settings include `SessionAuthentication`.
"""
from __future__ import unicode_literals
-from rest_framework.compat import patterns, url
+from django.conf.urls import patterns, url
+from django.contrib.auth import views
template_name = {'template_name': 'rest_framework/login.html'}
-urlpatterns = patterns('django.contrib.auth.views',
- url(r'^login/$', 'login', template_name, name='login'),
- url(r'^logout/$', 'logout', template_name, name='logout'),
+urlpatterns = patterns(
+ '',
+ url(r'^login/$', views.login, template_name, name='login'),
+ url(r'^logout/$', views.logout, template_name, name='logout')
)
diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py
index d51374b0a..e6690d170 100644
--- a/rest_framework/utils/breadcrumbs.py
+++ b/rest_framework/utils/breadcrumbs.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
from django.core.urlresolvers import resolve, get_script_prefix
-from rest_framework.utils.formatting import get_view_name
def get_breadcrumbs(url):
@@ -9,8 +8,11 @@ def get_breadcrumbs(url):
tuple of (name, url).
"""
+ from rest_framework.settings import api_settings
from rest_framework.views import APIView
+ view_name_func = api_settings.VIEW_NAME_FUNCTION
+
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
"""
Add tuples of (name, url) to the breadcrumbs list,
@@ -30,7 +32,7 @@ def get_breadcrumbs(url):
# Probably an optional trailing slash.
if not seen or seen[-1] != view:
suffix = getattr(view, 'suffix', None)
- name = get_view_name(view.cls, suffix)
+ name = view_name_func(cls, suffix)
breadcrumbs_list.insert(0, (name, prefix + url))
seen.append(view)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index b26a2085a..2160d18b6 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -2,96 +2,60 @@
Helper classes for parsers.
"""
from __future__ import unicode_literals
-from django.utils.datastructures import SortedDict
+from django.db.models.query import QuerySet
+from django.utils import six, timezone
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import timezone, force_text
-from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
+from rest_framework.compat import total_seconds
import datetime
import decimal
-import types
import json
+import uuid
class JSONEncoder(json.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time/timedelta,
- decimal types, and generators.
+ decimal types, generators and other basic python objects.
"""
- def default(self, o):
+ def default(self, obj):
# For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
- if isinstance(o, Promise):
- return force_text(o)
- elif isinstance(o, datetime.datetime):
- r = o.isoformat()
- if o.microsecond:
- r = r[:23] + r[26:]
- if r.endswith('+00:00'):
- r = r[:-6] + 'Z'
- return r
- elif isinstance(o, datetime.date):
- return o.isoformat()
- elif isinstance(o, datetime.time):
- if timezone and timezone.is_aware(o):
+ if isinstance(obj, Promise):
+ return force_text(obj)
+ elif isinstance(obj, datetime.datetime):
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:23] + representation[26:]
+ if representation.endswith('+00:00'):
+ representation = representation[:-6] + 'Z'
+ return representation
+ elif isinstance(obj, datetime.date):
+ return obj.isoformat()
+ elif isinstance(obj, datetime.time):
+ if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.")
- r = o.isoformat()
- if o.microsecond:
- r = r[:12]
- return r
- elif isinstance(o, datetime.timedelta):
- return str(o.total_seconds())
- elif isinstance(o, decimal.Decimal):
- return str(o)
- elif hasattr(o, '__iter__'):
- return [i for i in o]
- return super(JSONEncoder, self).default(o)
-
-
-try:
- import yaml
-except ImportError:
- SafeDumper = None
-else:
- # Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py
- class SafeDumper(yaml.SafeDumper):
- """
- Handles decimals as strings.
- Handles SortedDicts as usual dicts, but preserves field order, rather
- than the usual behaviour of sorting the keys.
- """
- def represent_decimal(self, data):
- return self.represent_scalar('tag:yaml.org,2002:str', str(data))
-
- def represent_mapping(self, tag, mapping, flow_style=None):
- value = []
- node = yaml.MappingNode(tag, value, flow_style=flow_style)
- if self.alias_key is not None:
- self.represented_objects[self.alias_key] = node
- best_style = True
- if hasattr(mapping, 'items'):
- mapping = list(mapping.items())
- if not isinstance(mapping, SortedDict):
- mapping.sort()
- for item_key, item_value in mapping:
- node_key = self.represent_data(item_key)
- node_value = self.represent_data(item_value)
- if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
- best_style = False
- if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style):
- best_style = False
- value.append((node_key, node_value))
- if flow_style is None:
- if self.default_flow_style is not None:
- node.flow_style = self.default_flow_style
- else:
- node.flow_style = best_style
- return node
-
- SafeDumper.add_representer(SortedDict,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(DictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(SortedDictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(types.GeneratorType,
- yaml.representer.SafeRepresenter.represent_list)
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:12]
+ return representation
+ elif isinstance(obj, datetime.timedelta):
+ return six.text_type(total_seconds(obj))
+ elif isinstance(obj, decimal.Decimal):
+ # Serializers will coerce decimals to strings by default.
+ return float(obj)
+ elif isinstance(obj, uuid.UUID):
+ return six.text_type(obj)
+ elif isinstance(obj, QuerySet):
+ return tuple(obj)
+ elif hasattr(obj, 'tolist'):
+ # Numpy arrays and array scalars.
+ return obj.tolist()
+ elif hasattr(obj, '__getitem__'):
+ try:
+ return dict(obj)
+ except:
+ pass
+ elif hasattr(obj, '__iter__'):
+ return tuple(item for item in obj)
+ return super(JSONEncoder, self).default(obj)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
new file mode 100644
index 000000000..c97ec5d0e
--- /dev/null
+++ b/rest_framework/utils/field_mapping.py
@@ -0,0 +1,249 @@
+"""
+Helper functions for mapping model fields to a dictionary of default
+keyword arguments that should be used for their equivelent serializer fields.
+"""
+from django.core import validators
+from django.db import models
+from django.utils.text import capfirst
+from rest_framework.compat import clean_manytomany_helptext
+from rest_framework.validators import UniqueValidator
+import inspect
+
+
+NUMERIC_FIELD_TYPES = (
+ models.IntegerField, models.FloatField, models.DecimalField
+)
+
+
+class ClassLookupDict(object):
+ """
+ Takes a dictionary with classes as keys.
+ Lookups against this object will traverses the object's inheritance
+ hierarchy in method resolution order, and returns the first matching value
+ from the dictionary or raises a KeyError if nothing matches.
+ """
+ def __init__(self, mapping):
+ self.mapping = mapping
+
+ def __getitem__(self, key):
+ if hasattr(key, '_proxy_class'):
+ # Deal with proxy classes. Ie. BoundField behaves as if it
+ # is a Field instance when using ClassLookupDict.
+ base_class = key._proxy_class
+ else:
+ base_class = key.__class__
+
+ for cls in inspect.getmro(base_class):
+ if cls in self.mapping:
+ return self.mapping[cls]
+ raise KeyError('Class %s not found in lookup.', cls.__name__)
+
+ def __setitem__(self, key, value):
+ self.mapping[key] = value
+
+
+def needs_label(model_field, field_name):
+ """
+ Returns `True` if the label based on the model's verbose name
+ is not equal to the default label it would have based on it's field name.
+ """
+ default_label = field_name.replace('_', ' ').capitalize()
+ return capfirst(model_field.verbose_name) != default_label
+
+
+def get_detail_view_name(model):
+ """
+ Given a model class, return the view name to use for URL relationships
+ that refer to instances of the model.
+ """
+ return '%(model_name)s-detail' % {
+ 'app_label': model._meta.app_label,
+ 'model_name': model._meta.object_name.lower()
+ }
+
+
+def get_field_kwargs(field_name, model_field):
+ """
+ Creates a default instance of a basic non-relational field.
+ """
+ kwargs = {}
+ validator_kwarg = list(model_field.validators)
+
+ # The following will only be used by ModelField classes.
+ # Gets removed for everything else.
+ kwargs['model_field'] = model_field
+
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+
+ if model_field.help_text:
+ kwargs['help_text'] = model_field.help_text
+
+ max_digits = getattr(model_field, 'max_digits', None)
+ if max_digits is not None:
+ kwargs['max_digits'] = max_digits
+
+ decimal_places = getattr(model_field, 'decimal_places', None)
+ if decimal_places is not None:
+ kwargs['decimal_places'] = decimal_places
+
+ if isinstance(model_field, models.TextField):
+ kwargs['style'] = {'base_template': 'textarea.html'}
+
+ if isinstance(model_field, models.AutoField) or not model_field.editable:
+ # If this field is read-only, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['read_only'] = True
+ return kwargs
+
+ if model_field.has_default() or model_field.blank or model_field.null:
+ kwargs['required'] = False
+
+ if model_field.null and not isinstance(model_field, models.NullBooleanField):
+ kwargs['allow_null'] = True
+
+ if model_field.blank:
+ kwargs['allow_blank'] = True
+
+ if model_field.flatchoices:
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['choices'] = model_field.flatchoices
+ return kwargs
+
+ # Ensure that max_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_length = getattr(model_field, 'max_length', None)
+ if max_length is not None and isinstance(model_field, models.CharField):
+ kwargs['max_length'] = max_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxLengthValidator)
+ ]
+
+ # Ensure that min_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_length = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinLengthValidator)
+ ), None)
+ if min_length is not None and isinstance(model_field, models.CharField):
+ kwargs['min_length'] = min_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinLengthValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MaxValueValidator)
+ ), None)
+ if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
+ kwargs['max_value'] = max_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxValueValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinValueValidator)
+ ), None)
+ if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
+ kwargs['min_value'] = min_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinValueValidator)
+ ]
+
+ # URLField does not need to include the URLValidator argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.URLField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.URLValidator)
+ ]
+
+ # EmailField does not need to include the validate_email argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.EmailField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_email
+ ]
+
+ # SlugField do not need to include the 'validate_slug' argument,
+ if isinstance(model_field, models.SlugField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_slug
+ ]
+
+ if getattr(model_field, 'unique', False):
+ validator = UniqueValidator(queryset=model_field.model._default_manager)
+ validator_kwarg.append(validator)
+
+ if validator_kwarg:
+ kwargs['validators'] = validator_kwarg
+
+ return kwargs
+
+
+def get_relation_kwargs(field_name, relation_info):
+ """
+ Creates a default instance of a flat relational field.
+ """
+ model_field, related_model, to_many, has_through_model = relation_info
+ kwargs = {
+ 'queryset': related_model._default_manager,
+ 'view_name': get_detail_view_name(related_model)
+ }
+
+ if to_many:
+ kwargs['many'] = True
+
+ if has_through_model:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+
+ if model_field:
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+ help_text = clean_manytomany_helptext(model_field.help_text)
+ if help_text:
+ kwargs['help_text'] = help_text
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+ if kwargs.get('read_only', False):
+ # If this field is read-only, then return early.
+ # No further keyword arguments are valid.
+ return kwargs
+ if model_field.has_default() or model_field.null:
+ kwargs['required'] = False
+ if model_field.null:
+ kwargs['allow_null'] = True
+ if model_field.validators:
+ kwargs['validators'] = model_field.validators
+ if getattr(model_field, 'unique', False):
+ validator = UniqueValidator(queryset=model_field.model._default_manager)
+ kwargs['validators'] = kwargs.get('validators', []) + [validator]
+
+ return kwargs
+
+
+def get_nested_relation_kwargs(relation_info):
+ kwargs = {'read_only': True}
+ if relation_info.to_many:
+ kwargs['many'] = True
+ return kwargs
+
+
+def get_url_kwargs(model_field):
+ return {
+ 'view_name': get_detail_view_name(model_field)
+ }
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 4bec83877..8b6f005e1 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -2,14 +2,13 @@
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
-
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown, smart_text
+from rest_framework.compat import apply_markdown, force_text
import re
-def _remove_trailing_string(content, trailing):
+def remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view classes.
@@ -19,11 +18,16 @@ def _remove_trailing_string(content, trailing):
return content
-def _remove_leading_indent(content):
+def dedent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
+
+ Note that python's `textwrap.dedent` doesn't quite cut it,
+ as it fails to dedent multiline docstrings that include
+ unindented text on the initial line.
"""
+ content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
@@ -31,11 +35,11 @@ def _remove_leading_indent(content):
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
- content = content.strip('\n')
- return content
+
+ return content.strip()
-def _camelcase_to_spaces(content):
+def camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes.
@@ -45,30 +49,6 @@ def _camelcase_to_spaces(content):
return ' '.join(content.split('_')).title()
-def get_view_name(cls, suffix=None):
- """
- Return a formatted name for an `APIView` class or `@api_view` function.
- """
- name = cls.__name__
- name = _remove_trailing_string(name, 'View')
- name = _remove_trailing_string(name, 'ViewSet')
- name = _camelcase_to_spaces(name)
- if suffix:
- name += ' ' + suffix
- return name
-
-
-def get_view_description(cls, html=False):
- """
- Return a description for an `APIView` class or `@api_view` function.
- """
- description = cls.__doc__ or ''
- description = _remove_leading_indent(smart_text(description))
- if html:
- return markup_description(description)
- return description
-
-
def markup_description(description):
"""
Apply HTML markup to the given description.
@@ -77,4 +57,5 @@ def markup_description(description):
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '
')
+ description = '' + description + '
'
return mark_safe(description)
diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py
new file mode 100644
index 000000000..d773952dc
--- /dev/null
+++ b/rest_framework/utils/html.py
@@ -0,0 +1,88 @@
+"""
+Helpers for dealing with HTML input.
+"""
+import re
+from django.utils.datastructures import MultiValueDict
+
+
+def is_html_input(dictionary):
+ # MultiDict type datastructures are used to represent HTML form input,
+ # which may have more than one value for each key.
+ return hasattr(dictionary, 'getlist')
+
+
+def parse_html_list(dictionary, prefix=''):
+ """
+ Used to suport list values in HTML forms.
+ Supports lists of primitives and/or dictionaries.
+
+ * List of primitives.
+
+ {
+ '[0]': 'abc',
+ '[1]': 'def',
+ '[2]': 'hij'
+ }
+ -->
+ [
+ 'abc',
+ 'def',
+ 'hij'
+ ]
+
+ * List of dictionaries.
+
+ {
+ '[0]foo': 'abc',
+ '[0]bar': 'def',
+ '[1]foo': 'hij',
+ '[1]bar': 'klm',
+ }
+ -->
+ [
+ {'foo': 'abc', 'bar': 'def'},
+ {'foo': 'hij', 'bar': 'klm'}
+ ]
+ """
+ ret = {}
+ regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
+ for field, value in dictionary.items():
+ match = regex.match(field)
+ if not match:
+ continue
+ index, key = match.groups()
+ index = int(index)
+ if not key:
+ ret[index] = value
+ elif isinstance(ret.get(index), dict):
+ ret[index][key] = value
+ else:
+ ret[index] = MultiValueDict({key: [value]})
+ return [ret[item] for item in sorted(ret.keys())]
+
+
+def parse_html_dict(dictionary, prefix):
+ """
+ Used to support dictionary values in HTML forms.
+
+ {
+ 'profile.username': 'example',
+ 'profile.email': 'example@example.com',
+ }
+ -->
+ {
+ 'profile': {
+ 'username': 'example',
+ 'email': 'example@example.com'
+ }
+ }
+ """
+ ret = {}
+ regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
+ for field, value in dictionary.items():
+ match = regex.match(field)
+ if not match:
+ continue
+ key = match.groups()[0]
+ ret[key] = value
+ return ret
diff --git a/rest_framework/utils/humanize_datetime.py b/rest_framework/utils/humanize_datetime.py
new file mode 100644
index 000000000..649f2abc6
--- /dev/null
+++ b/rest_framework/utils/humanize_datetime.py
@@ -0,0 +1,47 @@
+"""
+Helper functions that convert strftime formats into more readable representations.
+"""
+from rest_framework import ISO_8601
+
+
+def datetime_formats(formats):
+ format = ', '.join(formats).replace(
+ ISO_8601,
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
+ )
+ return humanize_strptime(format)
+
+
+def date_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
+ return humanize_strptime(format)
+
+
+def time_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
+ return humanize_strptime(format)
+
+
+def humanize_strptime(format_string):
+ # Note that we're missing some of the locale specific mappings that
+ # don't really make sense.
+ mapping = {
+ "%Y": "YYYY",
+ "%y": "YY",
+ "%m": "MM",
+ "%b": "[Jan-Dec]",
+ "%B": "[January-December]",
+ "%d": "DD",
+ "%H": "hh",
+ "%I": "hh", # Requires '%p' to differentiate from '%H'.
+ "%M": "mm",
+ "%S": "ss",
+ "%f": "uuuuuu",
+ "%a": "[Mon-Sun]",
+ "%A": "[Monday-Sunday]",
+ "%p": "[AM|PM]",
+ "%z": "[+HHMM|-HHMM]"
+ }
+ for key, val in mapping.items():
+ format_string = format_string.replace(key, val)
+ return format_string
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index c09c29338..de2931c28 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -5,6 +5,7 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
"""
from __future__ import unicode_literals
from django.http.multipartparser import parse_header
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import HTTP_HEADER_ENCODING
@@ -43,6 +44,7 @@ def order_by_precedence(media_type_lst):
return [media_types for media_types in ret if media_types]
+@python_2_unicode_compatible
class _MediaType(object):
def __init__(self, media_type_str):
if media_type_str is None:
@@ -57,7 +59,7 @@ class _MediaType(object):
if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
return False
- if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
+ if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
return False
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
@@ -74,14 +76,11 @@ class _MediaType(object):
return 0
elif self.sub_type == '*':
return 1
- elif not self.params or self.params.keys() == ['q']:
+ elif not self.params or list(self.params.keys()) == ['q']:
return 2
return 3
def __str__(self):
- return unicode(self).encode('utf-8')
-
- def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items():
ret += "; %s=%s" % (key, val)
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
new file mode 100644
index 000000000..d92bceb98
--- /dev/null
+++ b/rest_framework/utils/model_meta.py
@@ -0,0 +1,169 @@
+"""
+Helper function for returning the field information that is associated
+with a model class. This includes returning all the forward and reverse
+relationships and their associated metadata.
+
+Usage: `get_field_info(model)` returns a `FieldInfo` instance.
+"""
+from collections import namedtuple
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.utils import six
+from rest_framework.compat import OrderedDict
+import inspect
+
+
+FieldInfo = namedtuple('FieldResult', [
+ 'pk', # Model field instance
+ 'fields', # Dict of field name -> model field instance
+ 'forward_relations', # Dict of field name -> RelationInfo
+ 'reverse_relations', # Dict of field name -> RelationInfo
+ 'fields_and_pk', # Shortcut for 'pk' + 'fields'
+ 'relations' # Shortcut for 'forward_relations' + 'reverse_relations'
+])
+
+RelationInfo = namedtuple('RelationInfo', [
+ 'model_field',
+ 'related_model',
+ 'to_many',
+ 'has_through_model'
+])
+
+
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
+
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ resolved_model = models.get_model(app_name, model_name)
+ if resolved_model is None:
+ msg = "Django did not return a model for {0}.{1}"
+ raise ImproperlyConfigured(msg.format(app_name, model_name))
+ return resolved_model
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ raise ValueError("{0} is not a Django model".format(obj))
+
+
+def get_field_info(model):
+ """
+ Given a model class, returns a `FieldInfo` instance, which is a
+ `namedtuple`, containing metadata about the various field types on the model
+ including information about their relationships.
+ """
+ opts = model._meta.concrete_model._meta
+
+ pk = _get_pk(opts)
+ fields = _get_fields(opts)
+ forward_relations = _get_forward_relationships(opts)
+ reverse_relations = _get_reverse_relationships(opts)
+ fields_and_pk = _merge_fields_and_pk(pk, fields)
+ relationships = _merge_relationships(forward_relations, reverse_relations)
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations,
+ fields_and_pk, relationships)
+
+
+def _get_pk(opts):
+ pk = opts.pk
+ while pk.rel and pk.rel.parent_link:
+ # If model is a child via multi-table inheritance, use parent's pk.
+ pk = pk.rel.to._meta.pk
+
+ return pk
+
+
+def _get_fields(opts):
+ fields = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and not field.rel]:
+ fields[field.name] = field
+
+ return fields
+
+
+def _get_forward_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
+ forward_relations = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and field.rel]:
+ forward_relations[field.name] = RelationInfo(
+ model_field=field,
+ related_model=_resolve_model(field.rel.to),
+ to_many=False,
+ has_through_model=False
+ )
+
+ # Deal with forward many-to-many relationships.
+ for field in [field for field in opts.many_to_many if field.serialize]:
+ forward_relations[field.name] = RelationInfo(
+ model_field=field,
+ related_model=_resolve_model(field.rel.to),
+ to_many=True,
+ has_through_model=(
+ not field.rel.through._meta.auto_created
+ )
+ )
+
+ return forward_relations
+
+
+def _get_reverse_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
+ # Note that we have a hack here to handle internal API differences for
+ # this internal API across Django 1.7 -> Django 1.8.
+ # See: https://code.djangoproject.com/ticket/24208
+
+ reverse_relations = OrderedDict()
+ for relation in opts.get_all_related_objects():
+ accessor_name = relation.get_accessor_name()
+ related = getattr(relation, 'related_model', relation.model)
+ reverse_relations[accessor_name] = RelationInfo(
+ model_field=None,
+ related_model=related,
+ to_many=relation.field.rel.multiple,
+ has_through_model=False
+ )
+
+ # Deal with reverse many-to-many relationships.
+ for relation in opts.get_all_related_many_to_many_objects():
+ accessor_name = relation.get_accessor_name()
+ related = getattr(relation, 'related_model', relation.model)
+ reverse_relations[accessor_name] = RelationInfo(
+ model_field=None,
+ related_model=related,
+ to_many=True,
+ has_through_model=(
+ (getattr(relation.field.rel, 'through', None) is not None) and
+ not relation.field.rel.through._meta.auto_created
+ )
+ )
+
+ return reverse_relations
+
+
+def _merge_fields_and_pk(pk, fields):
+ fields_and_pk = OrderedDict()
+ fields_and_pk['pk'] = pk
+ fields_and_pk[pk.name] = pk
+ fields_and_pk.update(fields)
+
+ return fields_and_pk
+
+
+def _merge_relationships(forward_relations, reverse_relations):
+ return OrderedDict(
+ list(forward_relations.items()) +
+ list(reverse_relations.items())
+ )
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
new file mode 100644
index 000000000..1bfc64c1f
--- /dev/null
+++ b/rest_framework/utils/representation.py
@@ -0,0 +1,99 @@
+"""
+Helper functions for creating user-friendly representations
+of serializer classes and serializer fields.
+"""
+from __future__ import unicode_literals
+from django.db import models
+from django.utils.encoding import force_text
+from django.utils.functional import Promise
+from rest_framework.compat import unicode_repr
+import re
+
+
+def manager_repr(value):
+ model = value.model
+ opts = model._meta
+ for _, name, manager in opts.concrete_managers + opts.abstract_managers:
+ if manager == value:
+ return '%s.%s.all()' % (model._meta.object_name, name)
+ return repr(value)
+
+
+def smart_repr(value):
+ if isinstance(value, models.Manager):
+ return manager_repr(value)
+
+ if isinstance(value, Promise) and value._delegate_text:
+ value = force_text(value)
+
+ value = unicode_repr(value)
+
+ # Representations like u'help text'
+ # should simply be presented as 'help text'
+ if value.startswith("u'") and value.endswith("'"):
+ return value[1:]
+
+ # Representations like
+ #
+ # Should be presented as
+ #
+ value = re.sub(' at 0x[0-9a-f]{4,32}>', '>', value)
+
+ return value
+
+
+def field_repr(field, force_many=False):
+ kwargs = field._kwargs
+ if force_many:
+ kwargs = kwargs.copy()
+ kwargs['many'] = True
+ kwargs.pop('child', None)
+
+ arg_string = ', '.join([smart_repr(val) for val in field._args])
+ kwarg_string = ', '.join([
+ '%s=%s' % (key, smart_repr(val))
+ for key, val in sorted(kwargs.items())
+ ])
+ if arg_string and kwarg_string:
+ arg_string += ', '
+
+ if force_many:
+ class_name = force_many.__class__.__name__
+ else:
+ class_name = field.__class__.__name__
+
+ return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
+
+
+def serializer_repr(serializer, indent, force_many=None):
+ ret = field_repr(serializer, force_many) + ':'
+ indent_str = ' ' * indent
+
+ if force_many:
+ fields = force_many.fields
+ else:
+ fields = serializer.fields
+
+ for field_name, field in fields.items():
+ ret += '\n' + indent_str + field_name + ' = '
+ if hasattr(field, 'fields'):
+ ret += serializer_repr(field, indent + 1)
+ elif hasattr(field, 'child'):
+ ret += list_repr(field, indent + 1)
+ elif hasattr(field, 'child_relation'):
+ ret += field_repr(field.child_relation, force_many=field.child_relation)
+ else:
+ ret += field_repr(field)
+
+ if serializer.validators:
+ ret += '\n' + indent_str + 'class Meta:'
+ ret += '\n' + indent_str + ' validators = ' + smart_repr(serializer.validators)
+
+ return ret
+
+
+def list_repr(serializer, indent):
+ child = serializer.child
+ if hasattr(child, 'fields'):
+ return serializer_repr(serializer, indent, force_many=child)
+ return field_repr(serializer)
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
new file mode 100644
index 000000000..87bb3ac08
--- /dev/null
+++ b/rest_framework/utils/serializer_helpers.py
@@ -0,0 +1,120 @@
+from __future__ import unicode_literals
+import collections
+from rest_framework.compat import OrderedDict, unicode_to_repr
+
+
+class ReturnDict(OrderedDict):
+ """
+ Return object from `serialier.data` for the `Serializer` class.
+ Includes a backlink to the serializer instance for renderers
+ to use if they need richer field information.
+ """
+ def __init__(self, *args, **kwargs):
+ self.serializer = kwargs.pop('serializer')
+ super(ReturnDict, self).__init__(*args, **kwargs)
+
+ def copy(self):
+ return ReturnDict(self, serializer=self.serializer)
+
+ def __repr__(self):
+ return dict.__repr__(self)
+
+ def __reduce__(self):
+ # Pickling these objects will drop the .serializer backlink,
+ # but preserve the raw data.
+ return (dict, (dict(self),))
+
+
+class ReturnList(list):
+ """
+ Return object from `serialier.data` for the `SerializerList` class.
+ Includes a backlink to the serializer instance for renderers
+ to use if they need richer field information.
+ """
+ def __init__(self, *args, **kwargs):
+ self.serializer = kwargs.pop('serializer')
+ super(ReturnList, self).__init__(*args, **kwargs)
+
+ def __repr__(self):
+ return list.__repr__(self)
+
+ def __reduce__(self):
+ # Pickling these objects will drop the .serializer backlink,
+ # but preserve the raw data.
+ return (list, (list(self),))
+
+
+class BoundField(object):
+ """
+ A field object that also includes `.value` and `.error` properties.
+ Returned when iterating over a serializer instance,
+ providing an API similar to Django forms and form fields.
+ """
+ def __init__(self, field, value, errors, prefix=''):
+ self._field = field
+ self.value = value
+ self.errors = errors
+ self.name = prefix + self.field_name
+
+ def __getattr__(self, attr_name):
+ return getattr(self._field, attr_name)
+
+ @property
+ def _proxy_class(self):
+ return self._field.__class__
+
+ def __repr__(self):
+ return unicode_to_repr('<%s value=%s errors=%s>' % (
+ self.__class__.__name__, self.value, self.errors
+ ))
+
+
+class NestedBoundField(BoundField):
+ """
+ This `BoundField` additionally implements __iter__ and __getitem__
+ in order to support nested bound fields. This class is the type of
+ `BoundField` that is used for serializer fields.
+ """
+ def __iter__(self):
+ for field in self.fields.values():
+ yield self[field.field_name]
+
+ def __getitem__(self, key):
+ field = self.fields[key]
+ value = self.value.get(key) if self.value else None
+ error = self.errors.get(key) if self.errors else None
+ if hasattr(field, 'fields'):
+ return NestedBoundField(field, value, error, prefix=self.name + '.')
+ return BoundField(field, value, error, prefix=self.name + '.')
+
+
+class BindingDict(collections.MutableMapping):
+ """
+ This dict-like object is used to store fields on a serializer.
+
+ This ensures that whenever fields are added to the serializer we call
+ `field.bind()` so that the `field_name` and `parent` attributes
+ can be set correctly.
+ """
+ def __init__(self, serializer):
+ self.serializer = serializer
+ self.fields = OrderedDict()
+
+ def __setitem__(self, key, field):
+ self.fields[key] = field
+ field.bind(field_name=key, parent=self.serializer)
+
+ def __getitem__(self, key):
+ return self.fields[key]
+
+ def __delitem__(self, key):
+ del self.fields[key]
+
+ def __iter__(self):
+ return iter(self.fields)
+
+ def __len__(self):
+ return len(self.fields)
+
+ def __repr__(self):
+ return dict.__repr__(self.fields)
diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py
new file mode 100644
index 000000000..880ef9ed7
--- /dev/null
+++ b/rest_framework/utils/urls.py
@@ -0,0 +1,25 @@
+from django.utils.six.moves.urllib import parse as urlparse
+
+
+def replace_query_param(url, key, val):
+ """
+ Given a URL and a key/val pair, set or replace an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = urlparse.parse_qs(query)
+ query_dict[key] = [val]
+ query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+
+def remove_query_param(url, key):
+ """
+ Given a URL and a key/val pair, remove an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = urlparse.parse_qs(query)
+ query_dict.pop(key, None)
+ query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
new file mode 100644
index 000000000..6ae80b897
--- /dev/null
+++ b/rest_framework/validators.py
@@ -0,0 +1,261 @@
+"""
+We perform uniqueness checks explicitly on the serializer class, rather
+the using Django's `.full_clean()`.
+
+This gives us better separation of concerns, allows us to use single-step
+object creation, and makes it possible to switch between using the implicit
+`ModelSerializer` class and an equivalent explicit `Serializer` class.
+"""
+from __future__ import unicode_literals
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import unicode_to_repr
+from rest_framework.exceptions import ValidationError
+from rest_framework.utils.representation import smart_repr
+
+
+class UniqueValidator(object):
+ """
+ Validator that corresponds to `unique=True` on a model field.
+
+ Should be applied to an individual field on the serializer.
+ """
+ message = _('This field must be unique.')
+
+ def __init__(self, queryset, message=None):
+ self.queryset = queryset
+ self.serializer_field = None
+ self.message = message or self.message
+
+ def set_context(self, serializer_field):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the underlying model field name. This may not be the
+ # same as the serializer field name if `source=<>` is set.
+ self.field_name = serializer_field.source_attrs[0]
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer_field.parent, 'instance', None)
+
+ def filter_queryset(self, value, queryset):
+ """
+ Filter the queryset to all instances matching the given attribute.
+ """
+ filter_kwargs = {self.field_name: value}
+ return queryset.filter(**filter_kwargs)
+
+ def exclude_current_instance(self, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, value):
+ queryset = self.queryset
+ queryset = self.filter_queryset(value, queryset)
+ queryset = self.exclude_current_instance(queryset)
+ if queryset.exists():
+ raise ValidationError(self.message)
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset)
+ ))
+
+
+class UniqueTogetherValidator(object):
+ """
+ Validator that corresponds to `unique_together = (...)` on a model class.
+
+ Should be applied to the serializer class, not to an individual field.
+ """
+ message = _('The fields {field_names} must make a unique set.')
+ missing_message = _('This field is required.')
+
+ def __init__(self, queryset, fields, message=None):
+ self.queryset = queryset
+ self.fields = fields
+ self.serializer_field = None
+ self.message = message or self.message
+
+ def set_context(self, serializer):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer, 'instance', None)
+
+ def enforce_required_fields(self, attrs):
+ """
+ The `UniqueTogetherValidator` always forces an implied 'required'
+ state on the fields it applies to.
+ """
+ if self.instance is not None:
+ return
+
+ missing = dict([
+ (field_name, self.missing_message)
+ for field_name in self.fields
+ if field_name not in attrs
+ ])
+ if missing:
+ raise ValidationError(missing)
+
+ def filter_queryset(self, attrs, queryset):
+ """
+ Filter the queryset to all instances matching the given attributes.
+ """
+ # If this is an update, then any unprovided field should
+ # have it's value set based on the existing instance attribute.
+ if self.instance is not None:
+ for field_name in self.fields:
+ if field_name not in attrs:
+ attrs[field_name] = getattr(self.instance, field_name)
+
+ # Determine the filter keyword arguments and filter the queryset.
+ filter_kwargs = dict([
+ (field_name, attrs[field_name])
+ for field_name in self.fields
+ ])
+ return queryset.filter(**filter_kwargs)
+
+ def exclude_current_instance(self, attrs, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, attrs):
+ self.enforce_required_fields(attrs)
+ queryset = self.queryset
+ queryset = self.filter_queryset(attrs, queryset)
+ queryset = self.exclude_current_instance(attrs, queryset)
+
+ # Ignore validation if any field is None
+ checked_values = [
+ value for field, value in attrs.items() if field in self.fields
+ ]
+ if None not in checked_values and queryset.exists():
+ field_names = ', '.join(self.fields)
+ raise ValidationError(self.message.format(field_names=field_names))
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset),
+ smart_repr(self.fields)
+ ))
+
+
+class BaseUniqueForValidator(object):
+ message = None
+ missing_message = _('This field is required.')
+
+ def __init__(self, queryset, field, date_field, message=None):
+ self.queryset = queryset
+ self.field = field
+ self.date_field = date_field
+ self.message = message or self.message
+
+ def set_context(self, serializer):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the underlying model field names. These may not be the
+ # same as the serializer field names if `source=<>` is set.
+ self.field_name = serializer.fields[self.field].source_attrs[0]
+ self.date_field_name = serializer.fields[self.date_field].source_attrs[0]
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer, 'instance', None)
+
+ def enforce_required_fields(self, attrs):
+ """
+ The `UniqueForValidator` classes always force an implied
+ 'required' state on the fields they are applied to.
+ """
+ missing = dict([
+ (field_name, self.missing_message)
+ for field_name in [self.field, self.date_field]
+ if field_name not in attrs
+ ])
+ if missing:
+ raise ValidationError(missing)
+
+ def filter_queryset(self, attrs, queryset):
+ raise NotImplementedError('`filter_queryset` must be implemented.')
+
+ def exclude_current_instance(self, attrs, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, attrs):
+ self.enforce_required_fields(attrs)
+ queryset = self.queryset
+ queryset = self.filter_queryset(attrs, queryset)
+ queryset = self.exclude_current_instance(attrs, queryset)
+ if queryset.exists():
+ message = self.message.format(date_field=self.date_field)
+ raise ValidationError({self.field: message})
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset),
+ smart_repr(self.field),
+ smart_repr(self.date_field)
+ ))
+
+
+class UniqueForDateValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" date.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__day' % self.date_field_name] = date.day
+ filter_kwargs['%s__month' % self.date_field_name] = date.month
+ filter_kwargs['%s__year' % self.date_field_name] = date.year
+ return queryset.filter(**filter_kwargs)
+
+
+class UniqueForMonthValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" month.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__month' % self.date_field_name] = date.month
+ return queryset.filter(**filter_kwargs)
+
+
+class UniqueForYearValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" year.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__year' % self.date_field_name] = date.year
+ return queryset.filter(**filter_kwargs)
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
new file mode 100644
index 000000000..51b886f38
--- /dev/null
+++ b/rest_framework/versioning.py
@@ -0,0 +1,177 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import exceptions
+from rest_framework.compat import unicode_http_header
+from rest_framework.reverse import _reverse
+from rest_framework.settings import api_settings
+from rest_framework.templatetags.rest_framework import replace_query_param
+from rest_framework.utils.mediatypes import _MediaType
+import re
+
+
+class BaseVersioning(object):
+ default_version = api_settings.DEFAULT_VERSION
+ allowed_versions = api_settings.ALLOWED_VERSIONS
+ version_param = api_settings.VERSION_PARAM
+
+ def determine_version(self, request, *args, **kwargs):
+ msg = '{cls}.determine_version() must be implemented.'
+ raise NotImplementedError(msg.format(
+ cls=self.__class__.__name__
+ ))
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+ def is_allowed_version(self, version):
+ if not self.allowed_versions:
+ return True
+ return (version == self.default_version) or (version in self.allowed_versions)
+
+
+class AcceptHeaderVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json; version=1.0
+ """
+ invalid_version_message = _('Invalid version in "Accept" header.')
+
+ def determine_version(self, request, *args, **kwargs):
+ media_type = _MediaType(request.accepted_media_type)
+ version = media_type.params.get(self.version_param, self.default_version)
+ version = unicode_http_header(version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotAcceptable(self.invalid_version_message)
+ return version
+
+ # We don't need to implement `reverse`, as the versioning is based
+ # on the `Accept` header, not on the request URL.
+
+
+class URLPathVersioning(BaseVersioning):
+ """
+ To the client this is the same style as `NamespaceVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL keyword arguments to determine the version.
+
+ An example URL conf for two views that accept two different versions.
+
+ urlpatterns = [
+ url(r'^(?P{v1,v2})/users/$', users_list, name='users-list'),
+ url(r'^(?P{v1,v2})/users/(?P[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in URL path.')
+
+ def determine_version(self, request, *args, **kwargs):
+ version = kwargs.get(self.version_param, self.default_version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ kwargs = {} if (kwargs is None) else kwargs
+ kwargs[self.version_param] = request.version
+
+ return super(URLPathVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+
+
+class NamespaceVersioning(BaseVersioning):
+ """
+ To the client this is the same style as `URLPathVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL namespaces to determine the version.
+
+ An example URL conf that is namespaced into two seperate versions
+
+ # users/urls.py
+ urlpatterns = [
+ url(r'^/users/$', users_list, name='users-list'),
+ url(r'^/users/(?P[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
+ # urls.py
+ urlpatterns = [
+ url(r'^v1/', include('users.urls', namespace='v1')),
+ url(r'^v2/', include('users.urls', namespace='v2'))
+ ]
+
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in URL path.')
+
+ def determine_version(self, request, *args, **kwargs):
+ resolver_match = getattr(request, 'resolver_match', None)
+ if (resolver_match is None or not resolver_match.namespace):
+ return self.default_version
+ version = resolver_match.namespace
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ viewname = self.get_versioned_viewname(viewname, request)
+ return super(NamespaceVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+
+ def get_versioned_viewname(self, viewname, request):
+ return request.version + ':' + viewname
+
+
+class HostNameVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+ """
+ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
+ invalid_version_message = _('Invalid version in hostname.')
+
+ def determine_version(self, request, *args, **kwargs):
+ hostname, seperator, port = request.get_host().partition(':')
+ match = self.hostname_regex.match(hostname)
+ if not match:
+ return self.default_version
+ version = match.group(1)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ # We don't need to implement `reverse`, as the hostname will already be
+ # preserved as part of the REST framework `reverse` implementation.
+
+
+class QueryParameterVersioning(BaseVersioning):
+ """
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in query parameter.')
+
+ def determine_version(self, request, *args, **kwargs):
+ version = request.query_params.get(self.version_param)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ url = super(QueryParameterVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+ if request.version is not None:
+ return replace_query_param(url, self.version_param, request.version)
+ return url
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 37bba7f02..b4abc4d95 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -2,28 +2,106 @@
Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
-
from django.core.exceptions import PermissionDenied
from django.http import Http404
-from django.utils.datastructures import SortedDict
+from django.utils import six
+from django.utils.encoding import smart_text
+from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import View, HttpResponseBase
+from rest_framework.compat import HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.utils.formatting import get_view_name, get_view_description
+from rest_framework.utils import formatting
+import inspect
+import warnings
+
+
+def get_view_name(view_cls, suffix=None):
+ """
+ Given a view class, return a textual name to represent the view.
+ This name is used in the browsable API, and in OPTIONS responses.
+
+ This function is the default for the `VIEW_NAME_FUNCTION` setting.
+ """
+ name = view_cls.__name__
+ name = formatting.remove_trailing_string(name, 'View')
+ name = formatting.remove_trailing_string(name, 'ViewSet')
+ name = formatting.camelcase_to_spaces(name)
+ if suffix:
+ name += ' ' + suffix
+
+ return name
+
+
+def get_view_description(view_cls, html=False):
+ """
+ Given a view class, return a textual description to represent the view.
+ This name is used in the browsable API, and in OPTIONS responses.
+
+ This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
+ """
+ description = view_cls.__doc__ or ''
+ description = formatting.dedent(smart_text(description))
+ if html:
+ return formatting.markup_description(description)
+ return description
+
+
+def exception_handler(exc, context):
+ """
+ Returns the response that should be used for any given exception.
+
+ By default we handle the REST framework `APIException`, and also
+ Django's built-in `ValidationError`, `Http404` and `PermissionDenied`
+ exceptions.
+
+ Any unhandled exceptions may return `None`, which will cause a 500 error
+ to be raised.
+ """
+ if isinstance(exc, exceptions.APIException):
+ headers = {}
+ if getattr(exc, 'auth_header', None):
+ headers['WWW-Authenticate'] = exc.auth_header
+ if getattr(exc, 'wait', None):
+ headers['Retry-After'] = '%d' % exc.wait
+
+ if isinstance(exc.detail, (list, dict)):
+ data = exc.detail
+ else:
+ data = {'detail': exc.detail}
+
+ return Response(data, status=exc.status_code, headers=headers)
+
+ elif isinstance(exc, Http404):
+ msg = _('Not found.')
+ data = {'detail': six.text_type(msg)}
+ return Response(data, status=status.HTTP_404_NOT_FOUND)
+
+ elif isinstance(exc, PermissionDenied):
+ msg = _('Permission denied.')
+ data = {'detail': six.text_type(msg)}
+ return Response(data, status=status.HTTP_403_FORBIDDEN)
+
+ # Note: Unhandled exceptions will raise a 500 error.
+ return None
class APIView(View):
- settings = api_settings
+ # The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
+ metadata_class = api_settings.DEFAULT_METADATA_CLASS
+ versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
+
+ # Allow dependency injection of other settings to make testing easier.
+ settings = api_settings
@classmethod
def as_view(cls, **initkwargs):
@@ -35,7 +113,9 @@ class APIView(View):
"""
view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
- return view
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
+ return csrf_exempt(view)
@property
def allowed_methods(self):
@@ -46,12 +126,12 @@ class APIView(View):
@property
def default_response_headers(self):
- # TODO: deprecate?
- # TODO: Only vary by accept if multiple renderers
- return {
+ headers = {
'Allow': ', '.join(self.allowed_methods),
- 'Vary': 'Accept'
}
+ if len(self.renderer_classes) > 1:
+ headers['Vary'] = 'Accept'
+ return headers
def http_method_not_allowed(self, request, *args, **kwargs):
"""
@@ -64,7 +144,7 @@ class APIView(View):
"""
If request is not permitted, determine what kind of exception to raise.
"""
- if not self.request.successful_authenticator:
+ if not request.successful_authenticator:
raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied()
@@ -88,8 +168,8 @@ class APIView(View):
Returns a dict that is passed through to Parser.parse(),
as the `parser_context` keyword argument.
"""
- # Note: Additionally `request` will also be added to the context
- # by the Request object.
+ # Note: Additionally `request` and `encoding` will also be added
+ # to the context by the Request object.
return {
'view': self,
'args': getattr(self, 'args', ()),
@@ -110,6 +190,34 @@ class APIView(View):
'request': getattr(self, 'request', None)
}
+ def get_exception_handler_context(self):
+ """
+ Returns a dict that is passed through to EXCEPTION_HANDLER,
+ as the `context` argument.
+ """
+ return {
+ 'view': self,
+ 'args': getattr(self, 'args', ()),
+ 'kwargs': getattr(self, 'kwargs', {}),
+ 'request': getattr(self, 'request', None)
+ }
+
+ def get_view_name(self):
+ """
+ Return the view name, as used in OPTIONS responses and in the
+ browsable API.
+ """
+ func = self.settings.VIEW_NAME_FUNCTION
+ return func(self.__class__, getattr(self, 'suffix', None))
+
+ def get_view_description(self, html=False):
+ """
+ Return some descriptive text for the view, as used in OPTIONS responses
+ and in the browsable API.
+ """
+ func = self.settings.VIEW_DESCRIPTION_FUNCTION
+ return func(self.__class__, html)
+
# API policy instantiation methods
def get_format_suffix(self, **kwargs):
@@ -210,19 +318,31 @@ class APIView(View):
if not throttle.allow_request(request, self):
self.throttled(request, throttle.wait())
+ def determine_version(self, request, *args, **kwargs):
+ """
+ If versioning is being used, then determine any API version for the
+ incoming request. Returns a two-tuple of (version, versioning_scheme)
+ """
+ if self.versioning_class is None:
+ return (None, None)
+ scheme = self.versioning_class()
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
# Dispatch methods
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
- return Request(request,
- parsers=self.get_parsers(),
- authenticators=self.get_authenticators(),
- negotiator=self.get_content_negotiator(),
- parser_context=parser_context)
+ return Request(
+ request,
+ parsers=self.get_parsers(),
+ authenticators=self.get_authenticators(),
+ negotiator=self.get_content_negotiator(),
+ parser_context=parser_context
+ )
def initial(self, request, *args, **kwargs):
"""
@@ -239,6 +359,10 @@ class APIView(View):
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
+ # Determine the API version, if versioning is in use.
+ version, scheme = self.determine_version(request, *args, **kwargs)
+ request.version, request.versioning_scheme = version, scheme
+
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
@@ -269,37 +393,38 @@ class APIView(View):
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
- if isinstance(exc, exceptions.Throttled):
- # Throttle wait header
- self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
-
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request)
if auth_header:
- self.headers['WWW-Authenticate'] = auth_header
+ exc.auth_header = auth_header
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- if isinstance(exc, exceptions.APIException):
- return Response({'detail': exc.detail},
- status=exc.status_code,
- exception=True)
- elif isinstance(exc, Http404):
- return Response({'detail': 'Not found'},
- status=status.HTTP_404_NOT_FOUND,
- exception=True)
- elif isinstance(exc, PermissionDenied):
- return Response({'detail': 'Permission denied'},
- status=status.HTTP_403_FORBIDDEN,
- exception=True)
- raise
+ exception_handler = self.settings.EXCEPTION_HANDLER
- # Note: session based authentication is explicitly CSRF validated,
- # all other authentication is CSRF exempt.
- @csrf_exempt
+ if len(inspect.getargspec(exception_handler).args) == 1:
+ warnings.warn(
+ 'The `exception_handler(exc)` call signature is deprecated. '
+ 'Use `exception_handler(exc, context) instead.',
+ DeprecationWarning
+ )
+ response = exception_handler(exc)
+ else:
+ context = self.get_exception_handler_context()
+ response = exception_handler(exc, context)
+
+ if response is None:
+ raise
+
+ response.exception = True
+ return response
+
+ # Note: Views are made CSRF exempt from within `as_view` as to prevent
+ # accidental removal of this exemption in cases where `dispatch` needs to
+ # be overridden.
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
@@ -332,26 +457,8 @@ class APIView(View):
def options(self, request, *args, **kwargs):
"""
Handler method for HTTP 'OPTIONS' request.
- We may as well implement this as Django will otherwise provide
- a less useful default implementation.
"""
- return Response(self.metadata(request), status=status.HTTP_200_OK)
-
- def metadata(self, request):
- """
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
- """
-
- # This is used by ViewSets to disambiguate instance vs list views
- view_name_suffix = getattr(self, 'suffix', None)
-
- # By default we can't provide any form-like information, however the
- # generic views override this implementation and add additional
- # information for POST and PUT methods, based on the serializer.
- ret = SortedDict()
- ret['name'] = get_view_name(self.__class__, view_name_suffix)
- ret['description'] = get_view_description(self.__class__)
- ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
- ret['parses'] = [parser.media_type for parser in self.parser_classes]
- return ret
+ if self.metadata_class is None:
+ return self.http_method_not_allowed(request, *args, **kwargs)
+ data = self.metadata_class().determine_metadata(request, self)
+ return Response(data, status=status.HTTP_200_OK)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index d91323f22..88c763da4 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views.
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Typically, rather than instantiate views from viewsets directly, you'll
-regsiter the viewset with a router and let the URL conf be determined
+register the viewset with a router and let the URL conf be determined
automatically.
router = DefaultRouter()
@@ -20,6 +20,7 @@ from __future__ import unicode_literals
from functools import update_wrapper
from django.utils.decorators import classonlymethod
+from django.views.decorators.csrf import csrf_exempt
from rest_framework import views, generics, mixins
@@ -43,10 +44,16 @@ class ViewSetMixin(object):
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
- # The suffix initkwarg is reserved for identifing the viewset type
+ # The suffix initkwarg is reserved for identifying the viewset type
# eg. 'List' or 'Instance'.
cls.suffix = None
+ # actions must not be empty
+ if not actions:
+ raise TypeError("The `actions` argument must be provided when "
+ "calling `.as_view()` on a ViewSet. For example "
+ "`.as_view({'get': 'list'})`")
+
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
@@ -89,14 +96,14 @@ class ViewSetMixin(object):
# resolved URL.
view.cls = cls
view.suffix = initkwargs.get('suffix', None)
- return view
+ return csrf_exempt(view)
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Set the `.action` attribute on the view,
depending on the request method.
"""
- request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs)
+ request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
self.action = self.action_map.get(request.method.lower())
return request
@@ -127,11 +134,11 @@ class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
class ModelViewSet(mixins.CreateModelMixin,
- mixins.RetrieveModelMixin,
- mixins.UpdateModelMixin,
- mixins.DestroyModelMixin,
- mixins.ListModelMixin,
- GenericViewSet):
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
diff --git a/runtests.py b/runtests.py
new file mode 100755
index 000000000..0008bfae5
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,91 @@
+#! /usr/bin/env python
+from __future__ import print_function
+
+import pytest
+import sys
+import os
+import subprocess
+
+
+PYTEST_ARGS = {
+ 'default': ['tests', '--tb=short'],
+ 'fast': ['tests', '--tb=short', '-q'],
+}
+
+FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
+
+
+sys.path.append(os.path.dirname(__file__))
+
+
+def exit_on_failure(ret, message=None):
+ if ret:
+ sys.exit(ret)
+
+
+def flake8_main(args):
+ print('Running flake8 code linting')
+ ret = subprocess.call(['flake8'] + args)
+ print('flake8 failed' if ret else 'flake8 passed')
+ return ret
+
+
+def split_class_and_function(string):
+ class_string, function_string = string.split('.', 1)
+ return "%s and %s" % (class_string, function_string)
+
+
+def is_function(string):
+ # `True` if it looks like a test function is included in the string.
+ return string.startswith('test_') or '.test_' in string
+
+
+def is_class(string):
+ # `True` if first character is uppercase - assume it's a class name.
+ return string[0] == string[0].upper()
+
+
+if __name__ == "__main__":
+ try:
+ sys.argv.remove('--nolint')
+ except ValueError:
+ run_flake8 = True
+ else:
+ run_flake8 = False
+
+ try:
+ sys.argv.remove('--lintonly')
+ except ValueError:
+ run_tests = True
+ else:
+ run_tests = False
+
+ try:
+ sys.argv.remove('--fast')
+ except ValueError:
+ style = 'default'
+ else:
+ style = 'fast'
+ run_flake8 = False
+
+ if len(sys.argv) > 1:
+ pytest_args = sys.argv[1:]
+ first_arg = pytest_args[0]
+ if first_arg.startswith('-'):
+ # `runtests.py [flags]`
+ pytest_args = ['tests'] + pytest_args
+ elif is_class(first_arg) and is_function(first_arg):
+ # `runtests.py TestCase.test_function [flags]`
+ expression = split_class_and_function(first_arg)
+ pytest_args = ['tests', '-k', expression] + pytest_args[1:]
+ elif is_class(first_arg) or is_function(first_arg):
+ # `runtests.py TestCase [flags]`
+ # `runtests.py test_function [flags]`
+ pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:]
+ else:
+ pytest_args = PYTEST_ARGS[style]
+
+ if run_tests:
+ exit_on_failure(pytest.main(pytest_args))
+ if run_flake8:
+ exit_on_failure(flake8_main(FLAKE8_ARGS))
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 000000000..5e4090017
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index adf083cbf..4cdcfa86e 100755
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
from setuptools import setup
+from setuptools.command.test import test as TestCommand
import re
import os
import sys
@@ -12,7 +13,7 @@ def get_version(package):
Return package version as listed in `__version__` in `init.py`.
"""
init_py = open(os.path.join(package, '__init__.py')).read()
- return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
+ return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
def get_packages(package):
@@ -44,7 +45,14 @@ version = get_version('rest_framework')
if sys.argv[-1] == 'publish':
- os.system("python setup.py sdist upload")
+ if os.system("pip freeze | grep wheel"):
+ print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
+ sys.exit()
+ if os.system("pip freeze | grep twine"):
+ print("twine not installed.\nUse `pip install twine`.\nExiting.")
+ sys.exit()
+ os.system("python setup.py sdist bdist_wheel")
+ os.system("twine upload dist/*")
print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")
@@ -54,15 +62,15 @@ if sys.argv[-1] == 'publish':
setup(
name='djangorestframework',
version=version,
- url='http://django-rest-framework.org',
+ url='http://www.django-rest-framework.org',
license='BSD',
description='Web APIs for Django, made easy.',
author='Tom Christie',
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
packages=get_packages('rest_framework'),
package_data=get_package_data('rest_framework'),
- test_suite='rest_framework.runtests.runtests.main',
install_requires=[],
+ zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
diff --git a/rest_framework/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from rest_framework/tests/__init__.py
rename to tests/__init__.py
diff --git a/rest_framework/tests/extras/__init__.py b/tests/browsable_api/__init__.py
similarity index 100%
rename from rest_framework/tests/extras/__init__.py
rename to tests/browsable_api/__init__.py
diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py
new file mode 100644
index 000000000..97bc10360
--- /dev/null
+++ b/tests/browsable_api/auth_urls.py
@@ -0,0 +1,11 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+
+from .views import MockView
+
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+)
diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py
new file mode 100644
index 000000000..5e3604a66
--- /dev/null
+++ b/tests/browsable_api/no_auth_urls.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+)
diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py
new file mode 100644
index 000000000..5f2647838
--- /dev/null
+++ b/tests/browsable_api/test_browsable_api.py
@@ -0,0 +1,65 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from rest_framework.test import APIClient
+
+
+class DropdownWithAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views enabled."""
+
+ urls = 'tests.browsable_api.auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_logout_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, '>Log out<')
+
+ def test_login_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertContains(response, '>Log in<')
+
+
+class NoDropdownWithoutAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views NOT enabled."""
+
+ urls = 'tests.browsable_api.no_auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_dropdown_not_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertNotContains(response, '')
+
+ def test_dropdown_not_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertNotContains(response, ' ')
diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py
new file mode 100644
index 000000000..000f4e804
--- /dev/null
+++ b/tests/browsable_api/views.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from rest_framework.views import APIView
+from rest_framework import authentication
+from rest_framework import renderers
+from rest_framework.response import Response
+
+
+class MockView(APIView):
+
+ authentication_classes = (authentication.SessionAuthentication,)
+ renderer_classes = (renderers.BrowsableAPIRenderer,)
+
+ def get(self, request):
+ return Response({'a': 1, 'b': 2, 'c': 3})
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 000000000..44ed070b1
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,66 @@
+def pytest_configure():
+ from django.conf import settings
+
+ settings.configure(
+ DEBUG_PROPAGATE_EXCEPTIONS=True,
+ DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:'}},
+ SITE_ID=1,
+ SECRET_KEY='not very secret in tests',
+ USE_I18N=True,
+ USE_L10N=True,
+ STATIC_URL='/static/',
+ ROOT_URLCONF='tests.urls',
+ TEMPLATE_LOADERS=(
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ ),
+ MIDDLEWARE_CLASSES=(
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ ),
+ INSTALLED_APPS=(
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ 'rest_framework',
+ 'rest_framework.authtoken',
+ 'tests',
+ ),
+ PASSWORD_HASHERS=(
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+ ),
+ )
+
+ # guardian is optional
+ try:
+ import guardian # NOQA
+ except ImportError:
+ pass
+ else:
+ settings.ANONYMOUS_USER_ID = -1
+ settings.AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.ModelBackend',
+ 'guardian.backends.ObjectPermissionBackend',
+ )
+ settings.INSTALLED_APPS += (
+ 'guardian',
+ )
+
+ try:
+ import django
+ django.setup()
+ except AttributeError:
+ pass
diff --git a/rest_framework/tests/description.py b/tests/description.py
similarity index 100%
rename from rest_framework/tests/description.py
rename to tests/description.py
diff --git a/tests/models.py b/tests/models.py
new file mode 100644
index 000000000..456b0a0bb
--- /dev/null
+++ b/tests/models.py
@@ -0,0 +1,70 @@
+from __future__ import unicode_literals
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class RESTFrameworkModel(models.Model):
+ """
+ Base for test models that sets app_label, so they play nicely.
+ """
+
+ class Meta:
+ app_label = 'tests'
+ abstract = True
+
+
+class BasicModel(RESTFrameworkModel):
+ text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
+
+
+class BaseFilterableItem(RESTFrameworkModel):
+ text = models.CharField(max_length=100)
+
+ class Meta:
+ abstract = True
+
+
+class FilterableItem(BaseFilterableItem):
+ decimal = models.DecimalField(max_digits=4, decimal_places=2)
+ date = models.DateField()
+
+
+# Models for relations tests
+# ManyToMany
+class ManyToManyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+# ForeignKey
+class ForeignKeyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources',
+ help_text='Target', verbose_name='Target')
+
+
+# Nullable ForeignKey
+class NullableForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources',
+ verbose_name='Optional target object')
+
+
+# OneToOne
+class OneToOneTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class NullableOneToOneSource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
+ related_name='nullable_source')
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
new file mode 100644
index 000000000..91e49f9d8
--- /dev/null
+++ b/tests/test_authentication.py
@@ -0,0 +1,288 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+from django.contrib.auth.models import User
+from django.http import HttpResponse
+from django.test import TestCase
+from django.utils import six
+from rest_framework import HTTP_HEADER_ENCODING
+from rest_framework import exceptions
+from rest_framework import permissions
+from rest_framework import renderers
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.authentication import (
+ BaseAuthentication,
+ TokenAuthentication,
+ BasicAuthentication,
+ SessionAuthentication,
+)
+from rest_framework.authtoken.models import Token
+from rest_framework.test import APIRequestFactory, APIClient
+from rest_framework.views import APIView
+import base64
+
+factory = APIRequestFactory()
+
+
+class MockView(APIView):
+ permission_classes = (permissions.IsAuthenticated,)
+
+ def get(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+ def post(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+ def put(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+
+urlpatterns = patterns(
+ '',
+ (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
+ (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
+ (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
+ (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
+)
+
+
+class BasicAuthTests(TestCase):
+ """Basic authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def test_post_form_passing_basic_auth(self):
+ """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
+ credentials = ('%s:%s' % (self.username, self.password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ auth = 'Basic %s' % base64_credentials
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_passing_basic_auth(self):
+ """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
+ credentials = ('%s:%s' % (self.username, self.password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ auth = 'Basic %s' % base64_credentials
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_form_failing_basic_auth(self):
+ """Ensure POSTing form over basic auth without correct credentials fails"""
+ response = self.csrf_client.post('/basic/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_post_json_failing_basic_auth(self):
+ """Ensure POSTing json over basic auth without correct credentials fails"""
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+ self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
+
+
+class SessionAuthTests(TestCase):
+ """User session authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.non_csrf_client = APIClient(enforce_csrf_checks=False)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.csrf_client.logout()
+
+ def test_login_view_renders_on_get(self):
+ """
+ Ensure the login template renders for a basic GET.
+
+ cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
+ """
+ response = self.csrf_client.get('/auth/login/')
+ self.assertContains(response, '')
+
+ def test_post_form_session_auth_failing_csrf(self):
+ """
+ Ensure POSTing form over session authentication without CSRF token fails.
+ """
+ self.csrf_client.login(username=self.username, password=self.password)
+ response = self.csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_post_form_session_auth_passing(self):
+ """
+ Ensure POSTing form over session authentication with logged in user and CSRF token passes.
+ """
+ self.non_csrf_client.login(username=self.username, password=self.password)
+ response = self.non_csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_put_form_session_auth_passing(self):
+ """
+ Ensure PUTting form over session authentication with logged in user and CSRF token passes.
+ """
+ self.non_csrf_client.login(username=self.username, password=self.password)
+ response = self.non_csrf_client.put('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_form_session_auth_failing(self):
+ """
+ Ensure POSTing form over session authentication without logged in user fails.
+ """
+ response = self.csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+
+class TokenAuthTests(TestCase):
+ """Token authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ self.key = 'abcd1234'
+ self.token = Token.objects.create(key=self.key, user=self.user)
+
+ def test_post_form_passing_token_auth(self):
+ """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
+ auth = 'Token ' + self.key
+ response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_passing_token_auth(self):
+ """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
+ auth = "Token " + self.key
+ response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_makes_one_db_query(self):
+ """Ensure that authenticating a user using a token performs only one DB query"""
+ auth = "Token " + self.key
+
+ def func_to_test():
+ return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+
+ self.assertNumQueries(1, func_to_test)
+
+ def test_post_form_failing_token_auth(self):
+ """Ensure POSTing form over token auth without correct credentials fails"""
+ response = self.csrf_client.post('/token/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_post_json_failing_token_auth(self):
+ """Ensure POSTing json over token auth without correct credentials fails"""
+ response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_token_has_auto_assigned_key_if_none_provided(self):
+ """Ensure creating a token with no key will auto-assign a key"""
+ self.token.delete()
+ token = Token.objects.create(user=self.user)
+ self.assertTrue(bool(token.key))
+
+ def test_generate_key_returns_string(self):
+ """Ensure generate_key returns a string"""
+ token = Token()
+ key = token.generate_key()
+ self.assertTrue(isinstance(key, six.string_types))
+
+ def test_token_login_json(self):
+ """Ensure token login view using JSON POST works."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': self.password}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['token'], self.key)
+
+ def test_token_login_json_bad_creds(self):
+ """Ensure token login view using JSON POST fails if bad credentials are used."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': "badpass"}, format='json')
+ self.assertEqual(response.status_code, 400)
+
+ def test_token_login_json_missing_fields(self):
+ """Ensure token login view using JSON POST fails if missing fields."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username}, format='json')
+ self.assertEqual(response.status_code, 400)
+
+ def test_token_login_form(self):
+ """Ensure token login view using form POST works."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': self.password})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['token'], self.key)
+
+
+class IncorrectCredentialsTests(TestCase):
+ def test_incorrect_credentials(self):
+ """
+ If a request contains bad authentication credentials, then
+ authentication should run and error, even if no permissions
+ are set on the view.
+ """
+ class IncorrectCredentialsAuth(BaseAuthentication):
+ def authenticate(self, request):
+ raise exceptions.AuthenticationFailed('Bad credentials')
+
+ request = factory.get('/')
+ view = MockView.as_view(
+ authentication_classes=(IncorrectCredentialsAuth,),
+ permission_classes=()
+ )
+ response = view(request)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ self.assertEqual(response.data, {'detail': 'Bad credentials'})
+
+
+class FailingAuthAccessedInRenderer(TestCase):
+ def setUp(self):
+ class AuthAccessingRenderer(renderers.BaseRenderer):
+ media_type = 'text/plain'
+ format = 'txt'
+
+ def render(self, data, media_type=None, renderer_context=None):
+ request = renderer_context['request']
+ if request.user.is_authenticated():
+ return b'authenticated'
+ return b'not authenticated'
+
+ class FailingAuth(BaseAuthentication):
+ def authenticate(self, request):
+ raise exceptions.AuthenticationFailed('authentication failed')
+
+ class ExampleView(APIView):
+ authentication_classes = (FailingAuth,)
+ renderer_classes = (AuthAccessingRenderer,)
+
+ def get(self, request):
+ return Response({'foo': 'bar'})
+
+ self.view = ExampleView.as_view()
+
+ def test_failing_auth_accessed_in_renderer(self):
+ """
+ When authentication fails the renderer should still be able to access
+ `request.user` without raising an exception. Particularly relevant
+ to HTML responses that might reasonably access `request.user`.
+ """
+ request = factory.get('/')
+ response = self.view(request)
+ content = response.render().content
+ self.assertEqual(content, b'not authenticated')
diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py
new file mode 100644
index 000000000..bfc54b233
--- /dev/null
+++ b/tests/test_bound_fields.py
@@ -0,0 +1,69 @@
+from rest_framework import serializers
+
+
+class TestSimpleBoundField:
+ def test_empty_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer()
+
+ assert serializer['text'].value == ''
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is None
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+ def test_populated_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
+ assert serializer.is_valid()
+ assert serializer['text'].value == 'abc'
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is 123
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+ def test_error_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer(data={'text': 'x' * 1000, 'amount': 123})
+ serializer.is_valid()
+
+ assert serializer['text'].value == 'x' * 1000
+ assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is 123
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+
+class TestNestedBoundField:
+ def test_nested_empty_bound_field(self):
+ class Nested(serializers.Serializer):
+ more_text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ nested = Nested()
+
+ serializer = ExampleSerializer()
+
+ assert serializer['text'].value == ''
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['nested']['more_text'].value == ''
+ assert serializer['nested']['more_text'].errors is None
+ assert serializer['nested']['more_text'].name == 'nested.more_text'
+ assert serializer['nested']['amount'].value is None
+ assert serializer['nested']['amount'].errors is None
+ assert serializer['nested']['amount'].name == 'nested.amount'
diff --git a/rest_framework/tests/test_decorators.py b/tests/test_decorators.py
similarity index 100%
rename from rest_framework/tests/test_decorators.py
rename to tests/test_decorators.py
diff --git a/rest_framework/tests/test_description.py b/tests/test_description.py
similarity index 64%
rename from rest_framework/tests/test_description.py
rename to tests/test_description.py
index 8019f5eca..78ce2350b 100644
--- a/rest_framework/tests/test_description.py
+++ b/tests/test_description.py
@@ -2,11 +2,11 @@
from __future__ import unicode_literals
from django.test import TestCase
-from rest_framework.compat import apply_markdown, smart_text
+from django.utils.encoding import python_2_unicode_compatible, smart_text
+from rest_framework.compat import apply_markdown
from rest_framework.views import APIView
-from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
-from rest_framework.tests.description import UTF8_TEST_DOCSTRING
-from rest_framework.utils.formatting import get_view_name, get_view_description
+from .description import ViewWithNonASCIICharactersInDocstring
+from .description import UTF8_TEST_DOCSTRING
# We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring
@@ -58,7 +58,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_name(MockView), 'Mock')
+ self.assertEqual(MockView().get_view_name(), 'Mock')
def test_view_description_uses_docstring(self):
"""Ensure view descriptions are based on the docstring."""
@@ -78,7 +78,7 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #"""
- self.assertEqual(get_view_description(MockView), DESCRIPTION)
+ self.assertEqual(MockView().get_view_description(), DESCRIPTION)
def test_view_description_supports_unicode(self):
"""
@@ -86,7 +86,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
self.assertEqual(
- get_view_description(ViewWithNonASCIICharactersInDocstring),
+ ViewWithNonASCIICharactersInDocstring().get_view_description(),
smart_text(UTF8_TEST_DOCSTRING)
)
@@ -97,7 +97,29 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_description(MockView), '')
+ self.assertEqual(MockView().get_view_description(), '')
+
+ def test_view_description_can_be_promise(self):
+ """
+ Ensure a view may have a docstring that is actually a lazily evaluated
+ class that can be converted to a string.
+
+ See: https://github.com/tomchristie/django-rest-framework/issues/1708
+ """
+ # use a mock object instead of gettext_lazy to ensure that we can't end
+ # up with a test case string in our l10n catalog
+ @python_2_unicode_compatible
+ class MockLazyStr(object):
+ def __init__(self, string):
+ self.s = string
+
+ def __str__(self):
+ return self.s
+
+ class MockView(APIView):
+ __doc__ = MockLazyStr("a gettext string")
+
+ self.assertEqual(MockView().get_view_description(), 'a gettext string')
def test_markdown(self):
"""
diff --git a/tests/test_fields.py b/tests/test_fields.py
new file mode 100644
index 000000000..1aa528da6
--- /dev/null
+++ b/tests/test_fields.py
@@ -0,0 +1,1212 @@
+from decimal import Decimal
+from django.utils import timezone
+from rest_framework import serializers
+import datetime
+import django
+import pytest
+import uuid
+
+
+# Tests for field keyword arguments and core functionality.
+# ---------------------------------------------------------
+
+class TestEmpty:
+ """
+ Tests for `required`, `allow_null`, `allow_blank`, `default`.
+ """
+ def test_required(self):
+ """
+ By default a field must be included in the input.
+ """
+ field = serializers.IntegerField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation()
+ assert exc_info.value.detail == ['This field is required.']
+
+ def test_not_required(self):
+ """
+ If `required=False` then a field may be omitted from the input.
+ """
+ field = serializers.IntegerField(required=False)
+ with pytest.raises(serializers.SkipField):
+ field.run_validation()
+
+ def test_disallow_null(self):
+ """
+ By default `None` is not a valid input.
+ """
+ field = serializers.IntegerField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation(None)
+ assert exc_info.value.detail == ['This field may not be null.']
+
+ def test_allow_null(self):
+ """
+ If `allow_null=True` then `None` is a valid input.
+ """
+ field = serializers.IntegerField(allow_null=True)
+ output = field.run_validation(None)
+ assert output is None
+
+ def test_disallow_blank(self):
+ """
+ By default '' is not a valid input.
+ """
+ field = serializers.CharField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation('')
+ assert exc_info.value.detail == ['This field may not be blank.']
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = serializers.CharField(allow_blank=True)
+ output = field.run_validation('')
+ assert output == ''
+
+ def test_default(self):
+ """
+ If `default` is set, then omitted values get the default input.
+ """
+ field = serializers.IntegerField(default=123)
+ output = field.run_validation()
+ assert output is 123
+
+
+class TestSource:
+ def test_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='other')
+ serializer = ExampleSerializer(data={'example_field': 'abc'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'other': 'abc'}
+
+ def test_redundant_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_field')
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer().fields
+ assert str(exc_info.value) == (
+ "It is redundant to specify `source='example_field'` on field "
+ "'CharField' in serializer 'ExampleSerializer', because it is the "
+ "same as the field name. Remove the `source` keyword argument."
+ )
+
+ def test_callable_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_callable')
+
+ class ExampleInstance(object):
+ def example_callable(self):
+ return 'example callable value'
+
+ serializer = ExampleSerializer(ExampleInstance())
+ assert serializer.data['example_field'] == 'example callable value'
+
+ def test_callable_source_raises(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_callable', read_only=True)
+
+ class ExampleInstance(object):
+ def example_callable(self):
+ raise AttributeError('method call failed')
+
+ with pytest.raises(ValueError) as exc_info:
+ serializer = ExampleSerializer(ExampleInstance())
+ serializer.data.items()
+
+ assert 'method call failed' in str(exc_info.value)
+
+
+class TestReadOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ read_only = serializers.ReadOnlyField()
+ writable = serializers.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_read_only(self):
+ """
+ Read-only serializers.should not be included in validation.
+ """
+ data = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'writable': 456}
+
+ def test_serialize_read_only(self):
+ """
+ Read-only serializers.should be serialized.
+ """
+ instance = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'read_only': 123, 'writable': 456}
+
+
+class TestWriteOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ write_only = serializers.IntegerField(write_only=True)
+ readable = serializers.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_write_only(self):
+ """
+ Write-only serializers.should be included in validation.
+ """
+ data = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'write_only': 123, 'readable': 456}
+
+ def test_serialize_write_only(self):
+ """
+ Write-only serializers.should not be serialized.
+ """
+ instance = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'readable': 456}
+
+
+class TestInitial:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ initial_field = serializers.IntegerField(initial=123)
+ blank_field = serializers.IntegerField()
+ self.serializer = TestSerializer()
+
+ def test_initial(self):
+ """
+ Initial values should be included when serializing a new representation.
+ """
+ assert self.serializer.data == {
+ 'initial_field': 123,
+ 'blank_field': None
+ }
+
+
+class TestLabel:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ labeled = serializers.IntegerField(label='My label')
+ self.serializer = TestSerializer()
+
+ def test_label(self):
+ """
+ A field's label may be set with the `label` argument.
+ """
+ fields = self.serializer.fields
+ assert fields['labeled'].label == 'My label'
+
+
+class TestInvalidErrorKey:
+ def setup(self):
+ class ExampleField(serializers.Field):
+ def to_native(self, data):
+ self.fail('incorrect')
+ self.field = ExampleField()
+
+ def test_invalid_error_key(self):
+ """
+ If a field raises a validation error, but does not have a corresponding
+ error message, then raise an appropriate assertion error.
+ """
+ with pytest.raises(AssertionError) as exc_info:
+ self.field.to_native(123)
+ expected = (
+ 'ValidationError raised by `ExampleField`, but error key '
+ '`incorrect` does not exist in the `error_messages` dictionary.'
+ )
+ assert str(exc_info.value) == expected
+
+
+class TestBooleanHTMLInput:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ archived = serializers.BooleanField()
+ self.Serializer = TestSerializer
+
+ def test_empty_html_checkbox(self):
+ """
+ HTML checkboxes do not send any value, but should be treated
+ as `False` by BooleanField.
+ """
+ # This class mocks up a dictionary like object, that behaves
+ # as if it was returned for multipart or urlencoded data.
+ class MockHTMLDict(dict):
+ getlist = None
+ serializer = self.Serializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'archived': False}
+
+
+class MockHTMLDict(dict):
+ """
+ This class mocks up a dictionary like object, that behaves
+ as if it was returned for multipart or urlencoded data.
+ """
+ getlist = None
+
+
+class TestHTMLInput:
+ def test_empty_html_charfield(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(default='happy')
+
+ serializer = TestSerializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': 'happy'}
+
+ def test_empty_html_charfield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': None}
+
+ def test_empty_html_datefield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ expiry = serializers.DateField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'expiry': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'expiry': None}
+
+ def test_empty_html_charfield_allow_null_allow_blank(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(allow_null=True, allow_blank=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': ''}
+
+ def test_empty_html_charfield_required_false(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(required=False)
+
+ serializer = TestSerializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {}
+
+
+class TestCreateOnlyDefault:
+ def setup(self):
+ default = serializers.CreateOnlyDefault('2001-01-01')
+
+ class TestSerializer(serializers.Serializer):
+ published = serializers.HiddenField(default=default)
+ text = serializers.CharField()
+ self.Serializer = TestSerializer
+
+ def test_create_only_default_is_provided(self):
+ serializer = self.Serializer(data={'text': 'example'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'text': 'example', 'published': '2001-01-01'
+ }
+
+ def test_create_only_default_is_not_provided_on_update(self):
+ instance = {
+ 'text': 'example', 'published': '2001-01-01'
+ }
+ serializer = self.Serializer(instance, data={'text': 'example'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'text': 'example',
+ }
+
+ def test_create_only_default_callable_sets_context(self):
+ """
+ CreateOnlyDefault instances with a callable default should set_context
+ on the callable if possible
+ """
+ class TestCallableDefault:
+ def set_context(self, serializer_field):
+ self.field = serializer_field
+
+ def __call__(self):
+ return "success" if hasattr(self, 'field') else "failure"
+
+ class TestSerializer(serializers.Serializer):
+ context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault()))
+
+ serializer = TestSerializer(data={})
+ assert serializer.is_valid()
+ assert serializer.validated_data['context_set'] == 'success'
+
+
+# Tests for field input and output values.
+# ----------------------------------------
+
+def get_items(mapping_or_list_of_two_tuples):
+ # Tests accept either lists of two tuples, or dictionaries.
+ if isinstance(mapping_or_list_of_two_tuples, dict):
+ # {value: expected}
+ return mapping_or_list_of_two_tuples.items()
+ # [(value, expected), ...]
+ return mapping_or_list_of_two_tuples
+
+
+class FieldValues:
+ """
+ Base class for testing valid and invalid input values.
+ """
+ def test_valid_inputs(self):
+ """
+ Ensure that valid values return the expected validated data.
+ """
+ for input_value, expected_output in get_items(self.valid_inputs):
+ assert self.field.run_validation(input_value) == expected_output
+
+ def test_invalid_inputs(self):
+ """
+ Ensure that invalid values raise the expected validation error.
+ """
+ for input_value, expected_failure in get_items(self.invalid_inputs):
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ self.field.run_validation(input_value)
+ assert exc_info.value.detail == expected_failure
+
+ def test_outputs(self):
+ for output_value, expected_output in get_items(self.outputs):
+ assert self.field.to_representation(output_value) == expected_output
+
+
+# Boolean types...
+
+class TestBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ }
+ invalid_inputs = {
+ 'foo': ['"foo" is not a valid boolean.'],
+ None: ['This field may not be null.']
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ 'other': True
+ }
+ field = serializers.BooleanField()
+
+
+class TestNullBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None
+ }
+ invalid_inputs = {
+ 'foo': ['"foo" is not a valid boolean.'],
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None,
+ 'other': True
+ }
+ field = serializers.NullBooleanField()
+
+
+# String types...
+
+class TestCharField(FieldValues):
+ """
+ Valid and invalid values for `CharField`.
+ """
+ valid_inputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_inputs = {
+ '': ['This field may not be blank.']
+ }
+ outputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ field = serializers.CharField()
+
+ def test_trim_whitespace_default(self):
+ field = serializers.CharField()
+ assert field.to_internal_value(' abc ') == 'abc'
+
+ def test_trim_whitespace_disabled(self):
+ field = serializers.CharField(trim_whitespace=False)
+ assert field.to_internal_value(' abc ') == ' abc '
+
+
+class TestEmailField(FieldValues):
+ """
+ Valid and invalid values for `EmailField`.
+ """
+ valid_inputs = {
+ 'example@example.com': 'example@example.com',
+ ' example@example.com ': 'example@example.com',
+ }
+ invalid_inputs = {
+ 'examplecom': ['Enter a valid email address.']
+ }
+ outputs = {}
+ field = serializers.EmailField()
+
+
+class TestRegexField(FieldValues):
+ """
+ Valid and invalid values for `RegexField`.
+ """
+ valid_inputs = {
+ 'a9': 'a9',
+ }
+ invalid_inputs = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ outputs = {}
+ field = serializers.RegexField(regex='[a-z][0-9]')
+
+
+class TestSlugField(FieldValues):
+ """
+ Valid and invalid values for `SlugField`.
+ """
+ valid_inputs = {
+ 'slug-99': 'slug-99',
+ }
+ invalid_inputs = {
+ 'slug 99': ['Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.']
+ }
+ outputs = {}
+ field = serializers.SlugField()
+
+
+class TestURLField(FieldValues):
+ """
+ Valid and invalid values for `URLField`.
+ """
+ valid_inputs = {
+ 'http://example.com': 'http://example.com',
+ }
+ invalid_inputs = {
+ 'example.com': ['Enter a valid URL.']
+ }
+ outputs = {}
+ field = serializers.URLField()
+
+
+class TestUUIDField(FieldValues):
+ """
+ Valid and invalid values for `UUIDField`.
+ """
+ valid_inputs = {
+ '825d7aeb-05a9-45b5-a5b7-05df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'),
+ '825d7aeb05a945b5a5b705df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda')
+ }
+ invalid_inputs = {
+ '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.']
+ }
+ outputs = {
+ uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'): '825d7aeb-05a9-45b5-a5b7-05df87923cda'
+ }
+ field = serializers.UUIDField()
+
+
+# Number types...
+
+class TestIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField`.
+ """
+ valid_inputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ invalid_inputs = {
+ 'abc': ['A valid integer is required.']
+ }
+ outputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ field = serializers.IntegerField()
+
+
+class TestMinMaxIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 0: ['Ensure this value is greater than or equal to 1.'],
+ 4: ['Ensure this value is less than or equal to 3.'],
+ '0': ['Ensure this value is greater than or equal to 1.'],
+ '4': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = serializers.IntegerField(min_value=1, max_value=3)
+
+
+class TestFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField`.
+ """
+ valid_inputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ invalid_inputs = {
+ 'abc': ["A valid number is required."]
+ }
+ outputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ field = serializers.FloatField()
+
+
+class TestMinMaxFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ 1.0: 1.0,
+ 3.0: 3.0,
+ }
+ invalid_inputs = {
+ 0.9: ['Ensure this value is greater than or equal to 1.'],
+ 3.1: ['Ensure this value is less than or equal to 3.'],
+ '0.0': ['Ensure this value is greater than or equal to 1.'],
+ '3.1': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = serializers.FloatField(min_value=1, max_value=3)
+
+
+class TestDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField`.
+ """
+ valid_inputs = {
+ '12.3': Decimal('12.3'),
+ '0.1': Decimal('0.1'),
+ 10: Decimal('10'),
+ 0: Decimal('0'),
+ 12.3: Decimal('12.3'),
+ 0.1: Decimal('0.1'),
+ }
+ invalid_inputs = (
+ ('abc', ["A valid number is required."]),
+ (Decimal('Nan'), ["A valid number is required."]),
+ (Decimal('Inf'), ["A valid number is required."]),
+ ('12.345', ["Ensure that there are no more than 3 digits in total."]),
+ ('0.01', ["Ensure that there are no more than 1 decimal places."]),
+ (123, ["Ensure that there are no more than 2 digits before the decimal point."])
+ )
+ outputs = {
+ '1': '1.0',
+ '0': '0.0',
+ '1.09': '1.1',
+ '0.04': '0.0',
+ 1: '1.0',
+ 0: '0.0',
+ Decimal('1.0'): '1.0',
+ Decimal('0.0'): '0.0',
+ Decimal('1.09'): '1.1',
+ Decimal('0.04'): '0.0'
+ }
+ field = serializers.DecimalField(max_digits=3, decimal_places=1)
+
+
+class TestMinMaxDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField` with min and max limits.
+ """
+ valid_inputs = {
+ '10.0': Decimal('10.0'),
+ '20.0': Decimal('20.0'),
+ }
+ invalid_inputs = {
+ '9.9': ['Ensure this value is greater than or equal to 10.'],
+ '20.1': ['Ensure this value is less than or equal to 20.'],
+ }
+ outputs = {}
+ field = serializers.DecimalField(
+ max_digits=3, decimal_places=1,
+ min_value=10, max_value=20
+ )
+
+
+class TestNoStringCoercionDecimalField(FieldValues):
+ """
+ Output values for `DecimalField` with `coerce_to_string=False`.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ 1.09: Decimal('1.1'),
+ 0.04: Decimal('0.0'),
+ '1.09': Decimal('1.1'),
+ '0.04': Decimal('0.0'),
+ Decimal('1.09'): Decimal('1.1'),
+ Decimal('0.04'): Decimal('0.0'),
+ }
+ field = serializers.DecimalField(
+ max_digits=3, decimal_places=1,
+ coerce_to_string=False
+ )
+
+
+# Date & time serializers...
+
+class TestDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField`.
+ """
+ valid_inputs = {
+ '2001-01-01': datetime.date(2001, 1, 1),
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
+ datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
+ }
+ outputs = {
+ datetime.date(2001, 1, 1): '2001-01-01'
+ }
+ field = serializers.DateField()
+
+
+class TestCustomInputFormatDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1 Jan 2001': datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY.']
+ }
+ outputs = {}
+ field = serializers.DateField(input_formats=['%d %b %Y'])
+
+
+class TestCustomOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '01 Jan 2001'
+ }
+ field = serializers.DateField(format='%d %b %Y')
+
+
+class TestNoOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
+ }
+ field = serializers.DateField(format=None)
+
+
+class TestDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField`.
+ """
+ valid_inputs = {
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ # Django 1.4 does not support timezone string parsing.
+ '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
+ }
+ invalid_inputs = {
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
+ datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
+ }
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z'
+ }
+ field = serializers.DateTimeField(default_timezone=timezone.UTC())
+
+
+class TestCustomInputFormatDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
+ }
+ invalid_inputs = {
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
+ }
+ outputs = {}
+ field = serializers.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+
+
+class TestCustomOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
+ }
+ field = serializers.DateTimeField(format='%I:%M%p, %d %b %Y')
+
+
+class TestNoOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ field = serializers.DateTimeField(format=None)
+
+
+class TestNaiveDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with naive datetimes.
+ """
+ valid_inputs = {
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ invalid_inputs = {}
+ outputs = {}
+ field = serializers.DateTimeField(default_timezone=None)
+
+
+class TestTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField`.
+ """
+ valid_inputs = {
+ '13:00': datetime.time(13, 00),
+ datetime.time(13, 00): datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
+ }
+ outputs = {
+ datetime.time(13, 00): '13:00:00'
+ }
+ field = serializers.TimeField()
+
+
+class TestCustomInputFormatTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField` with a custom input format.
+ """
+ valid_inputs = {
+ '1:00pm': datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM].'],
+ }
+ outputs = {}
+ field = serializers.TimeField(input_formats=['%I:%M%p'])
+
+
+class TestCustomOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): '01:00PM'
+ }
+ field = serializers.TimeField(format='%I:%M%p')
+
+
+class TestNoOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): datetime.time(13, 00)
+ }
+ field = serializers.TimeField(format=None)
+
+
+# Choice types...
+
+class TestChoiceField(FieldValues):
+ """
+ Valid and invalid values for `ChoiceField`.
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'amazing': ['"amazing" is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good',
+ '': ''
+ }
+ field = serializers.ChoiceField(
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = serializers.ChoiceField(
+ allow_blank=True,
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+ output = field.run_validation('')
+ assert output == ''
+
+
+class TestChoiceFieldWithType(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses an integer type,
+ instead of a char type.
+ """
+ valid_inputs = {
+ '1': 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 5: ['"5" is not a valid choice.'],
+ 'abc': ['"abc" is not a valid choice.']
+ }
+ outputs = {
+ '1': 1,
+ 1: 1
+ }
+ field = serializers.ChoiceField(
+ choices=[
+ (1, 'Poor quality'),
+ (2, 'Medium quality'),
+ (3, 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithListChoices(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses a flat list for the
+ choices, rather than a list of pairs of (`value`, `description`).
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'awful': ['"awful" is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = serializers.ChoiceField(choices=('poor', 'medium', 'good'))
+
+
+class TestMultipleChoiceField(FieldValues):
+ """
+ Valid and invalid values for `MultipleChoiceField`.
+ """
+ valid_inputs = {
+ (): set(),
+ ('aircon',): set(['aircon']),
+ ('aircon', 'manual'): set(['aircon', 'manual']),
+ }
+ invalid_inputs = {
+ 'abc': ['Expected a list of items but got type "str".'],
+ ('aircon', 'incorrect'): ['"incorrect" is not a valid choice.']
+ }
+ outputs = [
+ (['aircon', 'manual'], set(['aircon', 'manual']))
+ ]
+ field = serializers.MultipleChoiceField(
+ choices=[
+ ('aircon', 'AirCon'),
+ ('manual', 'Manual drive'),
+ ('diesel', 'Diesel'),
+ ]
+ )
+
+
+# File serializers...
+
+class MockFile:
+ def __init__(self, name='', size=0, url=''):
+ self.name = name
+ self.size = size
+ self.url = url
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, MockFile) and
+ self.name == other.name and
+ self.size == other.size and
+ self.url == other.url
+ )
+
+
+class TestFileField(FieldValues):
+ """
+ Values for `FileField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example', size=10), MockFile(name='example', size=10))
+ ]
+ invalid_inputs = [
+ ('invalid', ['The submitted data was not a file. Check the encoding type on the form.']),
+ (MockFile(name='example.txt', size=0), ['The submitted file is empty.']),
+ (MockFile(name='', size=10), ['No filename could be determined.']),
+ (MockFile(name='x' * 100, size=10), ['Ensure this filename has at most 10 characters (it has 100).'])
+ ]
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), '/example.txt'),
+ ('', None)
+ ]
+ field = serializers.FileField(max_length=10)
+
+
+class TestFieldFieldWithName(FieldValues):
+ """
+ Values for `FileField` with a filename output instead of URLs.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), 'example.txt')
+ ]
+ field = serializers.FileField(use_url=False)
+
+
+# Stub out mock Django `forms.ImageField` class so we don't *actually*
+# call into it's regular validation, or require PIL for testing.
+class FailImageValidation(object):
+ def to_python(self, value):
+ raise serializers.ValidationError(self.error_messages['invalid_image'])
+
+
+class PassImageValidation(object):
+ def to_python(self, value):
+ return value
+
+
+class TestInvalidImageField(FieldValues):
+ """
+ Values for an invalid `ImageField`.
+ """
+ valid_inputs = {}
+ invalid_inputs = [
+ (MockFile(name='example.txt', size=10), ['Upload a valid image. The file you uploaded was either not an image or a corrupted image.'])
+ ]
+ outputs = {}
+ field = serializers.ImageField(_DjangoImageField=FailImageValidation)
+
+
+class TestValidImageField(FieldValues):
+ """
+ Values for an valid `ImageField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example.txt', size=10), MockFile(name='example.txt', size=10))
+ ]
+ invalid_inputs = {}
+ outputs = {}
+ field = serializers.ImageField(_DjangoImageField=PassImageValidation)
+
+
+# Composite serializers...
+
+class TestListField(FieldValues):
+ """
+ Values for `ListField` with IntegerField as child.
+ """
+ valid_inputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ invalid_inputs = [
+ ('not a list', ['Expected a list of items but got type "str".']),
+ ([1, 2, 'error'], ['A valid integer is required.'])
+ ]
+ outputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ field = serializers.ListField(child=serializers.IntegerField())
+
+
+class TestUnvalidatedListField(FieldValues):
+ """
+ Values for `ListField` with no `child` argument.
+ """
+ valid_inputs = [
+ ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+ ]
+ invalid_inputs = [
+ ('not a list', ['Expected a list of items but got type "str".']),
+ ]
+ outputs = [
+ ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+ ]
+ field = serializers.ListField()
+
+
+class TestDictField(FieldValues):
+ """
+ Values for `ListField` with CharField as child.
+ """
+ valid_inputs = [
+ ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+ ]
+ invalid_inputs = [
+ ({'a': 1, 'b': None}, ['This field may not be null.']),
+ ('not a dict', ['Expected a dictionary of items but got type "str".']),
+ ]
+ outputs = [
+ ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+ ]
+ field = serializers.DictField(child=serializers.CharField())
+
+
+class TestUnvalidatedDictField(FieldValues):
+ """
+ Values for `ListField` with no `child` argument.
+ """
+ valid_inputs = [
+ ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),
+ ]
+ invalid_inputs = [
+ ('not a dict', ['Expected a dictionary of items but got type "str".']),
+ ]
+ outputs = [
+ ({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}),
+ ]
+ field = serializers.DictField()
+
+
+# Tests for FieldField.
+# ---------------------
+
+class MockRequest:
+ def build_absolute_uri(self, value):
+ return 'http://example.com' + value
+
+
+class TestFileFieldContext:
+ def test_fully_qualified_when_request_in_context(self):
+ field = serializers.FileField(max_length=10)
+ field._context = {'request': MockRequest()}
+ obj = MockFile(name='example.txt', url='/example.txt')
+ value = field.to_representation(obj)
+ assert value == 'http://example.com/example.txt'
+
+
+# Tests for SerializerMethodField.
+# --------------------------------
+
+class TestSerializerMethodField:
+ def test_serializer_method_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField()
+
+ def get_example_field(self, obj):
+ return 'ran get_example_field(%d)' % obj['example_field']
+
+ serializer = ExampleSerializer({'example_field': 123})
+ assert serializer.data == {
+ 'example_field': 'ran get_example_field(123)'
+ }
+
+ def test_redundant_method_name(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField('get_example_field')
+
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer().fields
+ assert str(exc_info.value) == (
+ "It is redundant to specify `get_example_field` on "
+ "SerializerMethodField 'example_field' in serializer "
+ "'ExampleSerializer', because it is the same as the default "
+ "method name. Remove the `method_name` argument."
+ )
diff --git a/tests/test_filters.py b/tests/test_filters.py
new file mode 100644
index 000000000..e7cb0c795
--- /dev/null
+++ b/tests/test_filters.py
@@ -0,0 +1,823 @@
+from __future__ import unicode_literals
+import datetime
+from decimal import Decimal
+from django.db import models
+from django.conf.urls import patterns, url
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.utils import unittest
+from django.utils.dateparse import parse_date
+from django.utils.six.moves import reload_module
+from rest_framework import generics, serializers, status, filters
+from rest_framework.compat import django_filters
+from rest_framework.test import APIRequestFactory
+from .models import BaseFilterableItem, FilterableItem, BasicModel
+
+
+factory = APIRequestFactory()
+
+
+if django_filters:
+ class FilterableItemSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FilterableItem
+
+ # Basic filter on a list view.
+ class FilterFieldsRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_fields = ['decimal', 'date']
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These class are used to test a filter class.
+ class SeveralFieldsFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter(lookup_type='icontains')
+ decimal = django_filters.NumberFilter(lookup_type='lt')
+ date = django_filters.DateFilter(lookup_type='gt')
+
+ class Meta:
+ model = FilterableItem
+ fields = ['text', 'decimal', 'date']
+
+ class FilterClassRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These classes are used to test a misconfigured filter class.
+ class MisconfiguredFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter(lookup_type='icontains')
+
+ class Meta:
+ model = BasicModel
+ fields = ['text']
+
+ class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = MisconfiguredFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ class FilterClassDetailView(generics.RetrieveAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These classes are used to test base model filter support
+ class BaseFilterableItemFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter()
+
+ class Meta:
+ model = BaseFilterableItem
+
+ class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = BaseFilterableItemFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # Regression test for #814
+ class FilterFieldsQuerysetView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_fields = ['decimal', 'date']
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ class GetQuerysetView(generics.ListCreateAPIView):
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ def get_queryset(self):
+ return FilterableItem.objects.all()
+
+ urlpatterns = patterns(
+ '',
+ url(r'^(?P\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
+ url(r'^$', FilterClassRootView.as_view(), name='root-view'),
+ url(r'^get-queryset/$', GetQuerysetView.as_view(),
+ name='get-queryset-view'),
+ )
+
+
+class CommonFilteringTestCase(TestCase):
+ def _serialize_object(self, obj):
+ return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
+
+ def setUp(self):
+ """
+ Create 10 FilterableItem instances.
+ """
+ base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
+ for i in range(10):
+ text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
+ decimal = base_data[1] + i
+ date = base_data[2] - datetime.timedelta(days=i * 2)
+ FilterableItem(text=text, decimal=decimal, date=date).save()
+
+ self.objects = FilterableItem.objects
+ self.data = [
+ self._serialize_object(obj)
+ for obj in self.objects.all()
+ ]
+
+
+class IntegrationTestFiltering(CommonFilteringTestCase):
+ """
+ Integration tests for filtered list views.
+ """
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_fields_root_view(self):
+ """
+ GET requests to paginated ListCreateAPIView should return paginated results.
+ """
+ view = FilterFieldsRootView.as_view()
+
+ # Basic test with no filter.
+ request = factory.get('/')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, self.data)
+
+ # Tests that the decimal filter works.
+ search_decimal = Decimal('2.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the date filter works.
+ search_date = datetime.date(2012, 9, 22)
+ request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_filter_with_queryset(self):
+ """
+ Regression test for #814.
+ """
+ view = FilterFieldsQuerysetView.as_view()
+
+ # Tests that the decimal filter works.
+ search_decimal = Decimal('2.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_filter_with_get_queryset_only(self):
+ """
+ Regression test for #834.
+ """
+ view = GetQuerysetView.as_view()
+ request = factory.get('/get-queryset/')
+ view(request).render()
+ # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
+ # here when neither `model' nor `queryset' was specified.
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_class_root_view(self):
+ """
+ GET requests to filtered ListCreateAPIView that have a filter_class set
+ should return filtered results.
+ """
+ view = FilterClassRootView.as_view()
+
+ # Basic test with no filter.
+ request = factory.get('/')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, self.data)
+
+ # Tests that the decimal filter set with 'lt' in the filter class works.
+ search_decimal = Decimal('4.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the date filter set with 'gt' in the filter class works.
+ search_date = datetime.date(2012, 10, 2)
+ request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the text filter set with 'icontains' in the filter class works.
+ search_text = 'ff'
+ request = factory.get('/', {'text': '%s' % search_text})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if search_text in f['text'].lower()]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that multiple filters works.
+ search_decimal = Decimal('5.25')
+ search_date = datetime.date(2012, 10, 2)
+ request = factory.get('/', {
+ 'decimal': '%s' % (search_decimal,),
+ 'date': '%s' % (search_date,)
+ })
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
+ Decimal(f['decimal']) < search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_incorrectly_configured_filter(self):
+ """
+ An error should be displayed when the filter class is misconfigured.
+ """
+ view = IncorrectlyConfiguredRootView.as_view()
+
+ request = factory.get('/')
+ self.assertRaises(AssertionError, view, request)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_base_model_filter(self):
+ """
+ The `get_filter_class` model checks should allow base model filters.
+ """
+ view = BaseFilterableItemFilterRootView.as_view()
+
+ request = factory.get('/?text=aaa')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_unknown_filter(self):
+ """
+ GET requests with filters that aren't configured should return 200.
+ """
+ view = FilterFieldsRootView.as_view()
+
+ search_integer = 10
+ request = factory.get('/', {'integer': '%s' % search_integer})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+class IntegrationTestDetailFiltering(CommonFilteringTestCase):
+ """
+ Integration tests for filtered detail views.
+ """
+ urls = 'tests.test_filters'
+
+ def _get_url(self, item):
+ return reverse('detail-view', kwargs=dict(pk=item.pk))
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_detail_view(self):
+ """
+ GET requests to filtered RetrieveAPIView that have a filter_class set
+ should return filtered results.
+ """
+ item = self.objects.all()[0]
+ data = self._serialize_object(item)
+
+ # Basic test with no filter.
+ response = self.client.get(self._get_url(item))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, data)
+
+ # Tests that the decimal filter set that should fail.
+ search_decimal = Decimal('4.25')
+ high_item = self.objects.filter(decimal__gt=search_decimal)[0]
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(high_item)),
+ {'decimal': '{param}'.format(param=search_decimal)})
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Tests that the decimal filter set that should succeed.
+ search_decimal = Decimal('4.25')
+ low_item = self.objects.filter(decimal__lt=search_decimal)[0]
+ low_item_data = self._serialize_object(low_item)
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(low_item)),
+ {'decimal': '{param}'.format(param=search_decimal)})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, low_item_data)
+
+ # Tests that multiple filters works.
+ search_decimal = Decimal('5.25')
+ search_date = datetime.date(2012, 10, 2)
+ valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
+ valid_item_data = self._serialize_object(valid_item)
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(valid_item)), {
+ 'decimal': '{decimal}'.format(decimal=search_decimal),
+ 'date': '{date}'.format(date=search_date)
+ })
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, valid_item_data)
+
+
+class SearchFilterModel(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+
+
+class SearchFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SearchFilterModel
+
+
+class SearchFilterTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text is:
+ #
+ # z abc
+ # zz bcd
+ # zzz cde
+ # ...
+ for idx in range(10):
+ title = 'z' * (idx + 1)
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ SearchFilterModel(title=title, text=text).save()
+
+ def test_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'z', 'text': 'abc'},
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ def test_exact_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('=title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'zzz'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'zzz', 'text': 'cde'}
+ ]
+ )
+
+ def test_startswith_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', '^text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ def test_search_with_nonstandard_search_param(self):
+ with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}):
+ reload_module(filters)
+
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'query': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'z', 'text': 'abc'},
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ reload_module(filters)
+
+
+class AttributeModel(models.Model):
+ label = models.CharField(max_length=32)
+
+
+class SearchFilterModelM2M(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+ attributes = models.ManyToManyField(AttributeModel)
+
+
+class SearchFilterM2MSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SearchFilterModelM2M
+
+
+class SearchFilterM2MTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text/attributes is:
+ #
+ # z abc [1, 2, 3]
+ # zz bcd [1, 2, 3]
+ # zzz cde [1, 2, 3]
+ # ...
+ for idx in range(3):
+ label = 'w' * (idx + 1)
+ AttributeModel(label=label)
+
+ for idx in range(10):
+ title = 'z' * (idx + 1)
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ SearchFilterModelM2M(title=title, text=text).save()
+ SearchFilterModelM2M.objects.get(title='zz').attributes.add(1, 2, 3)
+
+ def test_m2m_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModelM2M.objects.all()
+ serializer_class = SearchFilterM2MSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('=title', 'text', 'attributes__label')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'zz'})
+ response = view(request)
+ self.assertEqual(len(response.data), 1)
+
+
+class OrderingFilterModel(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+
+
+class OrderingFilterRelatedModel(models.Model):
+ related_object = models.ForeignKey(OrderingFilterModel,
+ related_name="relateds")
+
+
+class OrderingFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OrderingFilterModel
+
+
+class DjangoFilterOrderingModel(models.Model):
+ date = models.DateField()
+ text = models.CharField(max_length=10)
+
+ class Meta:
+ ordering = ['-date']
+
+
+class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DjangoFilterOrderingModel
+
+
+class DjangoFilterOrderingTests(TestCase):
+ def setUp(self):
+ data = [{
+ 'date': datetime.date(2012, 10, 8),
+ 'text': 'abc'
+ }, {
+ 'date': datetime.date(2013, 10, 8),
+ 'text': 'bcd'
+ }, {
+ 'date': datetime.date(2014, 10, 8),
+ 'text': 'cde'
+ }]
+
+ for d in data:
+ DjangoFilterOrderingModel.objects.create(**d)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_default_ordering(self):
+ class DjangoFilterOrderingView(generics.ListAPIView):
+ serializer_class = DjangoFilterOrderingSerializer
+ queryset = DjangoFilterOrderingModel.objects.all()
+ filter_backends = (filters.DjangoFilterBackend,)
+ filter_fields = ['text']
+ ordering = ('-date',)
+
+ view = DjangoFilterOrderingView.as_view()
+ request = factory.get('/')
+ response = view(request)
+
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'date': '2014-10-08', 'text': 'cde'},
+ {'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
+ {'id': 1, 'date': '2012-10-08', 'text': 'abc'}
+ ]
+ )
+
+
+class OrderingFilterTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text is:
+ #
+ # zyx abc
+ # yxw bcd
+ # xwv cde
+ for idx in range(3):
+ title = (
+ chr(ord('z') - idx) +
+ chr(ord('y') - idx) +
+ chr(ord('x') - idx)
+ )
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ OrderingFilterModel(title=title, text=text).save()
+
+ def test_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ ]
+ )
+
+ def test_reverse_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': '-text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_incorrectfield_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'foobar'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ oredering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering_using_string(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_ordering_by_aggregate_field(self):
+ # create some related models to aggregate order by
+ num_objs = [2, 5, 3]
+ for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
+ num_objs):
+ for _ in range(num_relateds):
+ new_related = OrderingFilterRelatedModel(
+ related_object=obj
+ )
+ new_related.save()
+
+ class OrderingListView(generics.ListAPIView):
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ ordering_fields = '__all__'
+ queryset = OrderingFilterModel.objects.all().annotate(
+ models.Count("relateds"))
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'relateds__count'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ ]
+ )
+
+ def test_ordering_with_nonstandard_ordering_param(self):
+ with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}):
+ reload_module(filters)
+
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'order': 'text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ ]
+ )
+
+ reload_module(filters)
+
+
+class SensitiveOrderingFilterModel(models.Model):
+ username = models.CharField(max_length=20)
+ password = models.CharField(max_length=100)
+
+
+# Three different styles of serializer.
+# All should allow ordering by username, but not by password.
+class SensitiveDataSerializer1(serializers.ModelSerializer):
+ username = serializers.CharField()
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'username')
+
+
+class SensitiveDataSerializer2(serializers.ModelSerializer):
+ username = serializers.CharField()
+ password = serializers.CharField(write_only=True)
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'username', 'password')
+
+
+class SensitiveDataSerializer3(serializers.ModelSerializer):
+ user = serializers.CharField(source='username')
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'user')
+
+
+class SensitiveOrderingFilterTests(TestCase):
+ def setUp(self):
+ for idx in range(3):
+ username = {0: 'userA', 1: 'userB', 2: 'userC'}[idx]
+ password = {0: 'passA', 1: 'passC', 2: 'passB'}[idx]
+ SensitiveOrderingFilterModel(username=username, password=password).save()
+
+ def test_order_by_serializer_fields(self):
+ for serializer_cls in [
+ SensitiveDataSerializer1,
+ SensitiveDataSerializer2,
+ SensitiveDataSerializer3
+ ]:
+ class OrderingListView(generics.ListAPIView):
+ queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
+ filter_backends = (filters.OrderingFilter,)
+ serializer_class = serializer_cls
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': '-username'})
+ response = view(request)
+
+ if serializer_cls == SensitiveDataSerializer3:
+ username_field = 'user'
+ else:
+ username_field = 'username'
+
+ # Note: Inverse username ordering correctly applied.
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, username_field: 'userC'},
+ {'id': 2, username_field: 'userB'},
+ {'id': 1, username_field: 'userA'},
+ ]
+ )
+
+ def test_cannot_order_by_non_serializer_fields(self):
+ for serializer_cls in [
+ SensitiveDataSerializer1,
+ SensitiveDataSerializer2,
+ SensitiveDataSerializer3
+ ]:
+ class OrderingListView(generics.ListAPIView):
+ queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
+ filter_backends = (filters.OrderingFilter,)
+ serializer_class = serializer_cls
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'password'})
+ response = view(request)
+
+ if serializer_cls == SensitiveDataSerializer3:
+ username_field = 'user'
+ else:
+ username_field = 'username'
+
+ # Note: The passwords are not in order. Default ordering is used.
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, username_field: 'userA'}, # PassB
+ {'id': 2, username_field: 'userB'}, # PassC
+ {'id': 3, username_field: 'userC'}, # PassA
+ ]
+ )
diff --git a/rest_framework/tests/test_generics.py b/tests/test_generics.py
similarity index 70%
rename from rest_framework/tests/test_generics.py
rename to tests/test_generics.py
index 1550880b5..88e792cea 100644
--- a/rest_framework/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,46 +1,75 @@
from __future__ import unicode_literals
+import django
from django.db import models
from django.shortcuts import get_object_or_404
from django.test import TestCase
+from django.utils import six
from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
-from rest_framework.compat import six
+from tests.models import BasicModel, RESTFrameworkModel
+from tests.models import ForeignKeySource, ForeignKeyTarget
factory = APIRequestFactory()
-class RootView(generics.ListCreateAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
+# Models
+class SlugBasedModel(RESTFrameworkModel):
+ text = models.CharField(max_length=100)
+ slug = models.SlugField(max_length=32)
-class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
+# Model for regression test for #285
+class Comment(RESTFrameworkModel):
+ email = models.EmailField()
+ content = models.CharField(max_length=200)
+ created = models.DateTimeField(auto_now_add=True)
+
+
+# Serializers
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class ForeignKeySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
class SlugSerializer(serializers.ModelSerializer):
- slug = serializers.Field() # read only
+ slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
- exclude = ('id',)
+ fields = ('text', 'slug')
+
+
+# Views
+class RootView(generics.ListCreateAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+
+
+class InstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicModel.objects.exclude(text='filtered out')
+ serializer_class = BasicSerializer
+
+
+class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ForeignKeySource.objects.all()
+ serializer_class = ForeignKeySerializer
class SlugBasedInstanceView(InstanceView):
"""
A model with a slug-field.
"""
- model = SlugBasedModel
+ queryset = SlugBasedModel.objects.all()
serializer_class = SlugSerializer
lookup_field = 'slug'
+# Tests
class TestRootView(TestCase):
def setUp(self):
"""
@@ -88,7 +117,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'PUT' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'})
def test_delete_root_view(self):
"""
@@ -98,48 +127,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
-
- def test_options_root_view(self):
- """
- OPTIONS requests to ListCreateAPIView should return metadata
- """
- request = factory.options('/')
- with self.assertNumQueries(0):
- response = self.view(request).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Root',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'POST': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- "label": "Text comes here",
- "help_text": "Text description."
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
+ self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'})
def test_post_cannot_set_id(self):
"""
@@ -155,15 +143,18 @@ class TestRootView(TestCase):
self.assertEqual(created.text, 'foobar')
+EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
+
+
class TestInstanceView(TestCase):
def setUp(self):
"""
- Create 3 BasicModel intances.
+ Create 3 BasicModel instances.
"""
- items = ['foo', 'bar', 'baz']
+ items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
BasicModel(text=item).save()
- self.objects = BasicModel.objects
+ self.objects = BasicModel.objects.exclude(text='filtered out')
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
@@ -190,7 +181,7 @@ class TestInstanceView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'})
def test_put_instance_view(self):
"""
@@ -198,10 +189,10 @@ class TestInstanceView(TestCase):
"""
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk='1').render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
+ self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
self.assertEqual(updated.text, 'foobar')
@@ -212,7 +203,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'}
request = factory.patch('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -231,47 +222,6 @@ class TestInstanceView(TestCase):
ids = [obj.id for obj in self.objects.all()]
self.assertEqual(ids, [2, 3])
- def test_options_instance_view(self):
- """
- OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- """
- request = factory.options('/1')
- with self.assertNumQueries(1):
- response = self.view(request, pk=1).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Instance',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'PUT': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- 'label': 'Text comes here',
- 'help_text': 'Text description.'
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
-
def test_get_instance_view_incorrect_arg(self):
"""
GET requests with an incorrect pk type, should raise 404, not 500.
@@ -288,7 +238,7 @@ class TestInstanceView(TestCase):
"""
data = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -297,46 +247,56 @@ class TestInstanceView(TestCase):
def test_put_to_deleted_instance(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- if it does not currently exist.
+ PUT requests to RetrieveUpdateDestroyAPIView should return 404 if
+ an object does not currently exist.
"""
self.objects.get(id=1).delete()
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(3):
+ with self.assertNumQueries(1):
response = self.view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
- updated = self.objects.get(id=1)
- self.assertEqual(updated.text, 'foobar')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- def test_put_as_create_on_id_based_url(self):
+ def test_put_to_filtered_out_instance(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if it doesn't exist.
+ PUT requests to an URL of instance which is filtered out should not be
+ able to create new objects.
+ """
+ data = {'text': 'foo'}
+ filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
+ request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
+ response = self.view(request, pk=filtered_out_pk).render()
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_patch_cannot_create_an_object(self):
+ """
+ PATCH requests should not be able to create objects.
"""
data = {'text': 'foobar'}
- # pk fields can not be created on demand, only the database can set the pk for a new object
- request = factory.put('/5', data, format='json')
- with self.assertNumQueries(3):
- response = self.view(request, pk=5).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- new_obj = self.objects.get(pk=5)
- self.assertEqual(new_obj.text, 'foobar')
+ request = factory.patch('/999', data, format='json')
+ with self.assertNumQueries(1):
+ response = self.view(request, pk=999).render()
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertFalse(self.objects.filter(id=999).exists())
- def test_put_as_create_on_slug_based_url(self):
+
+class TestFKInstanceView(TestCase):
+ def setUp(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
+ Create 3 BasicModel instances.
"""
- data = {'text': 'foobar'}
- request = factory.put('/test_slug', data, format='json')
- with self.assertNumQueries(2):
- response = self.slug_based_view(request, slug='test_slug').render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
- new_obj = SlugBasedModel.objects.get(slug='test_slug')
- self.assertEqual(new_obj.text, 'foobar')
+ items = ['foo', 'bar', 'baz']
+ for item in items:
+ t = ForeignKeyTarget(name=item)
+ t.save()
+ ForeignKeySource(name='source_' + item, target=t).save()
+
+ self.objects = ForeignKeySource.objects
+ self.data = [
+ {'id': obj.id, 'name': obj.name}
+ for obj in self.objects.all()
+ ]
+ self.view = FKInstanceView.as_view()
class TestOverriddenGetObject(TestCase):
@@ -344,9 +304,10 @@ class TestOverriddenGetObject(TestCase):
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
queryset/model mechanism but instead overrides get_object()
"""
+
def setUp(self):
"""
- Create 3 BasicModel intances.
+ Create 3 BasicModel instances.
"""
items = ['foo', 'bar', 'baz']
for item in items:
@@ -361,7 +322,7 @@ class TestOverriddenGetObject(TestCase):
"""
Example detail view for override of get_object().
"""
- model = BasicModel
+ serializer_class = BasicSerializer
def get_object(self):
pk = int(self.kwargs['pk'])
@@ -419,11 +380,13 @@ class ClassB(models.Model):
class ClassA(models.Model):
name = models.CharField(max_length=255)
- childs = models.ManyToManyField(ClassB, blank=True, null=True)
+ children = models.ManyToManyField(ClassB, blank=True, null=True)
class ClassASerializer(serializers.ModelSerializer):
- childs = serializers.PrimaryKeyRelatedField(many=True, source='childs')
+ children = serializers.PrimaryKeyRelatedField(
+ many=True, queryset=ClassB.objects.all()
+ )
class Meta:
model = ClassA
@@ -431,11 +394,11 @@ class ClassASerializer(serializers.ModelSerializer):
class ExampleView(generics.ListCreateAPIView):
serializer_class = ClassASerializer
- model = ClassA
+ queryset = ClassA.objects.all()
-class TestM2MBrowseableAPI(TestCase):
- def test_m2m_in_browseable_api(self):
+class TestM2MBrowsableAPI(TestCase):
+ def test_m2m_in_browsable_api(self):
"""
Test for particularly ugly regression with m2m in browsable API
"""
@@ -455,8 +418,29 @@ class ExclusiveFilterBackend(object):
return queryset.filter(text='other')
-class TestFilterBackendAppliedToViews(TestCase):
+class TwoFieldModel(models.Model):
+ field_a = models.CharField(max_length=100)
+ field_b = models.CharField(max_length=100)
+
+class DynamicSerializerView(generics.ListCreateAPIView):
+ queryset = TwoFieldModel.objects.all()
+ renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
+
+ def get_serializer_class(self):
+ if self.request.method == 'POST':
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ fields = ('field_b',)
+ else:
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ return DynamicSerializer
+
+
+class TestFilterBackendAppliedToViews(TestCase):
def setUp(self):
"""
Create 3 BasicModel instances to filter on.
@@ -499,7 +483,7 @@ class TestFilterBackendAppliedToViews(TestCase):
request = factory.get('/1')
response = instance_view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- self.assertEqual(response.data, {'detail': 'Not found'})
+ self.assertEqual(response.data, {'detail': 'Not found.'})
def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self):
"""
@@ -511,28 +495,6 @@ class TestFilterBackendAppliedToViews(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
-
-class TwoFieldModel(models.Model):
- field_a = models.CharField(max_length=100)
- field_b = models.CharField(max_length=100)
-
-
-class DynamicSerializerView(generics.ListCreateAPIView):
- model = TwoFieldModel
- renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
-
- def get_serializer_class(self):
- if self.request.method == 'POST':
- class DynamicSerializer(serializers.ModelSerializer):
- class Meta:
- model = TwoFieldModel
- fields = ('field_b',)
- return DynamicSerializer
- return super(DynamicSerializerView, self).get_serializer_class()
-
-
-class TestFilterBackendAppliedToViews(TestCase):
-
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
diff --git a/rest_framework/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py
similarity index 82%
rename from rest_framework/tests/test_htmlrenderer.py
rename to tests/test_htmlrenderer.py
index 8957a43c7..a33b832f5 100644
--- a/rest_framework/tests/test_htmlrenderer.py
+++ b/tests/test_htmlrenderer.py
@@ -1,15 +1,15 @@
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
+from django.conf.urls import patterns, url
from django.http import Http404
from django.test import TestCase
from django.template import TemplateDoesNotExist, Template
-import django.template.loader
+from django.utils import six
from rest_framework import status
-from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
-from rest_framework.compat import six
+import django.template.loader
@api_view(('GET',))
@@ -34,7 +34,8 @@ def not_found(request):
raise Http404()
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^$', example),
url(r'^permission_denied$', permission_denied),
url(r'^not_found$', not_found),
@@ -42,7 +43,7 @@ urlpatterns = patterns('',
class TemplateHTMLRendererTests(TestCase):
- urls = 'rest_framework.tests.test_htmlrenderer'
+ urls = 'tests.test_htmlrenderer'
def setUp(self):
"""
@@ -50,12 +51,18 @@ class TemplateHTMLRendererTests(TestCase):
"""
self.get_template = django.template.loader.get_template
- def get_template(template_name):
+ def get_template(template_name, dirs=None):
if template_name == 'example.html':
return Template("example: {{ object }}")
raise TemplateDoesNotExist(template_name)
+ def select_template(template_name_list, dirs=None, using=None):
+ if template_name_list == ['example.html']:
+ return Template("example: {{ object }}")
+ raise TemplateDoesNotExist(template_name_list[0])
+
django.template.loader.get_template = get_template
+ django.template.loader.select_template = select_template
def tearDown(self):
"""
@@ -82,7 +89,7 @@ class TemplateHTMLRendererTests(TestCase):
class TemplateHTMLRendererExceptionTests(TestCase):
- urls = 'rest_framework.tests.test_htmlrenderer'
+ urls = 'tests.test_htmlrenderer'
def setUp(self):
"""
@@ -108,11 +115,13 @@ class TemplateHTMLRendererExceptionTests(TestCase):
def test_not_found_html_view_with_template(self):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- self.assertEqual(response.content, six.b("404: Not found"))
+ self.assertTrue(response.content in (
+ six.b("404: Not found"), six.b("404 Not Found")))
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- self.assertEqual(response.content, six.b("403: Permission denied"))
+ self.assertTrue(response.content in (
+ six.b("403: Permission denied"), six.b("403 Forbidden")))
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
new file mode 100644
index 000000000..3a435f02f
--- /dev/null
+++ b/tests/test_metadata.py
@@ -0,0 +1,209 @@
+from __future__ import unicode_literals
+from rest_framework import exceptions, serializers, status, views, versioning
+from rest_framework.request import Request
+from rest_framework.renderers import BrowsableAPIRenderer
+from rest_framework.test import APIRequestFactory
+
+request = Request(APIRequestFactory().options('/'))
+
+
+class TestMetadata:
+ def test_metadata(self):
+ """
+ OPTIONS requests to views should return a valid 200 response.
+ """
+ class ExampleView(views.APIView):
+ """Example view."""
+ pass
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ]
+ }
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == expected
+
+ def test_none_metadata(self):
+ """
+ OPTIONS requests to views where `metadata_class = None` should raise
+ a MethodNotAllowed exception, which will result in an HTTP 405 response.
+ """
+ class ExampleView(views.APIView):
+ metadata_class = None
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
+ assert response.data == {'detail': 'Method "OPTIONS" not allowed.'}
+
+ def test_actions(self):
+ """
+ On generic views OPTIONS should return an 'actions' key with metadata
+ on the fields that may be supplied to PUT and POST requests.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(
+ min_value=1, max_value=1000
+ )
+ char_field = serializers.CharField(
+ required=False, min_length=3, max_length=40
+ )
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ],
+ 'actions': {
+ 'POST': {
+ 'choice_field': {
+ 'type': 'choice',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Choice field',
+ 'choices': [
+ {'display_name': 'red', 'value': 'red'},
+ {'display_name': 'green', 'value': 'green'},
+ {'display_name': 'blue', 'value': 'blue'}
+ ]
+ },
+ 'integer_field': {
+ 'type': 'integer',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Integer field',
+ 'min_value': 1,
+ 'max_value': 1000,
+
+ },
+ 'char_field': {
+ 'type': 'string',
+ 'required': False,
+ 'read_only': False,
+ 'label': 'Char field',
+ 'min_length': 3,
+ 'max_length': 40
+ }
+ }
+ }
+ }
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == expected
+
+ def test_global_permissions(self):
+ """
+ If a user does not have global permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def check_permissions(self, request):
+ if request.method == 'POST':
+ raise exceptions.PermissionDenied()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_200_OK
+ assert list(response.data['actions'].keys()) == ['PUT']
+
+ def test_object_permissions(self):
+ """
+ If a user does not have object permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def get_object(self):
+ if self.request.method == 'PUT':
+ raise exceptions.PermissionDenied()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_200_OK
+ assert list(response.data['actions'].keys()) == ['POST']
+
+ def test_bug_2455_clone_request(self):
+ class ExampleView(views.APIView):
+ renderer_classes = (BrowsableAPIRenderer,)
+
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ assert hasattr(self.request, 'version')
+ return serializers.Serializer()
+
+ view = ExampleView.as_view()
+ view(request=request)
+
+ def test_bug_2477_clone_request(self):
+ class ExampleView(views.APIView):
+ renderer_classes = (BrowsableAPIRenderer,)
+
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ assert hasattr(self.request, 'versioning_scheme')
+ return serializers.Serializer()
+
+ scheme = versioning.QueryParameterVersioning
+ view = ExampleView.as_view(versioning_class=scheme)
+ view(request=request)
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
new file mode 100644
index 000000000..4c099fca1
--- /dev/null
+++ b/tests/test_middleware.py
@@ -0,0 +1,37 @@
+
+from django.conf.urls import patterns, url
+from django.contrib.auth.models import User
+from rest_framework.authentication import TokenAuthentication
+from rest_framework.authtoken.models import Token
+from rest_framework.test import APITestCase
+from rest_framework.views import APIView
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', APIView.as_view(authentication_classes=(TokenAuthentication,))),
+)
+
+
+class MyMiddleware(object):
+
+ def process_response(self, request, response):
+ assert hasattr(request, 'user'), '`user` is not set on request'
+ assert request.user.is_authenticated(), '`user` is not authenticated'
+ return response
+
+
+class TestMiddleware(APITestCase):
+
+ urls = 'tests.test_middleware'
+
+ def test_middleware_can_access_user_when_processing_response(self):
+ user = User.objects.create_user('john', 'john@example.com', 'password')
+ key = 'abcd1234'
+ Token.objects.create(key=key, user=user)
+
+ with self.settings(
+ MIDDLEWARE_CLASSES=('tests.test_middleware.MyMiddleware',)
+ ):
+ auth = 'Token ' + key
+ self.client.get('/', HTTP_AUTHORIZATION=auth)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
new file mode 100644
index 000000000..bce2008a8
--- /dev/null
+++ b/tests/test_model_serializer.py
@@ -0,0 +1,641 @@
+"""
+The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
+shortcuts for automatically creating serializers based on a given model class.
+
+These tests deal with ensuring that we correctly map the model fields onto
+an appropriate set of serializer fields for each case.
+"""
+from __future__ import unicode_literals
+from django.core.exceptions import ImproperlyConfigured
+from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
+from django.db import models
+from django.test import TestCase
+from django.utils import six
+from rest_framework import serializers
+from rest_framework.compat import unicode_repr
+
+
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Tests for regular field mappings.
+# ---------------------------------
+
+class CustomField(models.Field):
+ """
+ A custom model field simply for testing purposes.
+ """
+ pass
+
+
+class OneFieldModel(models.Model):
+ char_field = models.CharField(max_length=100)
+
+
+class RegularFieldsModel(models.Model):
+ """
+ A model class for testing regular flat fields.
+ """
+ auto_field = models.AutoField(primary_key=True)
+ big_integer_field = models.BigIntegerField()
+ boolean_field = models.BooleanField(default=False)
+ char_field = models.CharField(max_length=100)
+ comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
+ date_field = models.DateField()
+ datetime_field = models.DateTimeField()
+ decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
+ email_field = models.EmailField(max_length=100)
+ float_field = models.FloatField()
+ integer_field = models.IntegerField()
+ null_boolean_field = models.NullBooleanField()
+ positive_integer_field = models.PositiveIntegerField()
+ positive_small_integer_field = models.PositiveSmallIntegerField()
+ slug_field = models.SlugField(max_length=100)
+ small_integer_field = models.SmallIntegerField()
+ text_field = models.TextField()
+ time_field = models.TimeField()
+ url_field = models.URLField(max_length=100)
+ custom_field = CustomField()
+
+ def method(self):
+ return 'method'
+
+
+COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
+
+
+class FieldOptionsModel(models.Model):
+ value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
+ length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12)
+ blank_field = models.CharField(blank=True, max_length=10)
+ null_field = models.IntegerField(null=True)
+ default_field = models.IntegerField(default=0)
+ descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label')
+ choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
+
+
+class TestModelSerializer(TestCase):
+ def test_create_method(self):
+ class TestSerializer(serializers.ModelSerializer):
+ non_model_field = serializers.CharField()
+
+ class Meta:
+ model = OneFieldModel
+ fields = ('char_field', 'non_model_field')
+
+ serializer = TestSerializer(data={
+ 'char_field': 'foo',
+ 'non_model_field': 'bar',
+ })
+ serializer.is_valid()
+ with self.assertRaises(TypeError) as excinfo:
+ serializer.save()
+ msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.'
+ assert str(excinfo.exception).startswith(msginitial)
+
+
+class TestRegularFieldMappings(TestCase):
+ def test_regular_fields(self):
+ """
+ Model fields should map to their equivelent serializer fields.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ big_integer_field = IntegerField()
+ boolean_field = BooleanField(required=False)
+ char_field = CharField(max_length=100)
+ comma_separated_integer_field = CharField(max_length=100, validators=[])
+ date_field = DateField()
+ datetime_field = DateTimeField()
+ decimal_field = DecimalField(decimal_places=1, max_digits=3)
+ email_field = EmailField(max_length=100)
+ float_field = FloatField()
+ integer_field = IntegerField()
+ null_boolean_field = NullBooleanField(required=False)
+ positive_integer_field = IntegerField()
+ positive_small_integer_field = IntegerField()
+ slug_field = SlugField(max_length=100)
+ small_integer_field = IntegerField()
+ text_field = CharField(style={'base_template': 'textarea.html'})
+ time_field = TimeField()
+ url_field = URLField(max_length=100)
+ custom_field = ModelField(model_field=)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_field_options(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FieldOptionsModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ value_limit_field = IntegerField(max_value=10, min_value=1)
+ length_limit_field = CharField(max_length=12, min_length=3)
+ blank_field = CharField(allow_blank=True, max_length=10, required=False)
+ null_field = IntegerField(allow_null=True, required=False)
+ default_field = IntegerField(required=False)
+ descriptive_field = IntegerField(help_text='Some help text', label='A label')
+ choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
+ """)
+ if six.PY2:
+ # This particular case is too awkward to resolve fully across
+ # both py2 and py3.
+ expected = expected.replace(
+ "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')",
+ "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')"
+ )
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_method_field(self):
+ """
+ Properties and methods on the model should be allowed as `Meta.fields`
+ values, and should map to `ReadOnlyField`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'method')
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ method = ReadOnlyField()
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_pk_fields(self):
+ """
+ Both `pk` and the actual primary key name are valid in `Meta.fields`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('pk', 'auto_field')
+
+ expected = dedent("""
+ TestSerializer():
+ pk = IntegerField(label='Auto field', read_only=True)
+ auto_field = IntegerField(read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_extra_field_kwargs(self):
+ """
+ Ensure `extra_kwargs` are passed to generated fields.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'char_field')
+ extra_kwargs = {'char_field': {'default': 'extra'}}
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ char_field = CharField(default='extra', max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_invalid_field(self):
+ """
+ Field names that do not map to a model field or relationship should
+ raise a configuration errror.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'invalid')
+
+ with self.assertRaises(ImproperlyConfigured) as excinfo:
+ TestSerializer().fields
+ expected = 'Field name `invalid` is not valid for model `RegularFieldsModel`.'
+ assert str(excinfo.exception) == expected
+
+ def test_missing_field(self):
+ """
+ Fields that have been declared on the serializer class must be included
+ in the `Meta.fields` if it exists.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field',)
+
+ with self.assertRaises(AssertionError) as excinfo:
+ TestSerializer().fields
+ expected = (
+ "The field 'missing' was declared on serializer TestSerializer, "
+ "but has not been included in the 'fields' option."
+ )
+ assert str(excinfo.exception) == expected
+
+ def test_missing_superclass_field(self):
+ """
+ Fields that have been declared on a parent of the serializer class may
+ be excluded from the `Meta.fields` option.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+
+ class ChildSerializer(TestSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field',)
+
+ ChildSerializer().fields
+
+
+# Tests for relational field mappings.
+# ------------------------------------
+
+class ForeignKeyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class OneToOneTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ThroughTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Supplementary(models.Model):
+ extra = models.IntegerField()
+ forwards = models.ForeignKey('ThroughTargetModel')
+ backwards = models.ForeignKey('RelationalModel')
+
+
+class RelationalModel(models.Model):
+ foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
+ many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
+ one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
+ through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
+
+
+class TestRelationalFieldMappings(TestCase):
+ def test_pk_relations(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[])
+ many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
+ through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_nested_relations(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_hyperlinked_relations(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[], view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_nested_hyperlinked_relations(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_foreign_key(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeyTargetModel
+ fields = ('id', 'name', 'reverse_foreign_key')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_one_to_one(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OneToOneTargetModel
+ fields = ('id', 'name', 'reverse_one_to_one')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_many_to_many(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyTargetModel
+ fields = ('id', 'name', 'reverse_many_to_many')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_through(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ThroughTargetModel
+ fields = ('id', 'name', 'reverse_through')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+
+class TestIntegration(TestCase):
+ def setUp(self):
+ self.foreign_key_target = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ self.one_to_one_target = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ self.many_to_many_targets = [
+ ManyToManyTargetModel.objects.create(
+ name='many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ self.instance = RelationalModel.objects.create(
+ foreign_key=self.foreign_key_target,
+ one_to_one=self.one_to_one_target,
+ )
+ self.instance.many_to_many = self.many_to_many_targets
+ self.instance.save()
+
+ def test_pk_retrival(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ serializer = TestSerializer(self.instance)
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': self.foreign_key_target.pk,
+ 'one_to_one': self.one_to_one_target.pk,
+ 'many_to_many': [item.pk for item in self.many_to_many_targets],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+ def test_pk_create(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = TestSerializer(data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+ def test_pk_update(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = TestSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+
+# Tests for bulk create using `ListSerializer`.
+
+class BulkCreateModel(models.Model):
+ name = models.CharField(max_length=10)
+
+
+class TestBulkCreate(TestCase):
+ def test_bulk_create(self):
+ class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BulkCreateModel
+ fields = ('name',)
+
+ class BulkCreateSerializer(serializers.ListSerializer):
+ child = BasicModelSerializer()
+
+ data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}]
+ serializer = BulkCreateSerializer(data=data)
+ assert serializer.is_valid()
+
+ # Objects are returned by save().
+ instances = serializer.save()
+ assert len(instances) == 3
+ assert [item.name for item in instances] == ['a', 'b', 'c']
+
+ # Objects have been created in the database.
+ assert BulkCreateModel.objects.count() == 3
+ assert list(BulkCreateModel.objects.values_list('name', flat=True)) == ['a', 'b', 'c']
+
+ # Serializer returns correct data.
+ assert serializer.data == data
+
+
+class TestMetaClassModel(models.Model):
+ text = models.CharField(max_length=100)
+
+
+class TestSerializerMetaClass(TestCase):
+ def test_meta_class_fields_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `fields` option must be a list or tuple"
+ )
+
+ def test_meta_class_exclude_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ exclude = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `exclude` option must be a list or tuple"
+ )
+
+ def test_meta_class_fields_and_exclude_options(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = ('text',)
+ exclude = ('text',)
+
+ with self.assertRaises(AssertionError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ self.assertEqual(
+ str(exception),
+ "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer."
+ )
diff --git a/rest_framework/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py
similarity index 88%
rename from rest_framework/tests/test_multitable_inheritance.py
rename to tests/test_multitable_inheritance.py
index 00c153276..15627e1dd 100644
--- a/rest_framework/tests/test_multitable_inheritance.py
+++ b/tests/test_multitable_inheritance.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import RESTFrameworkModel
+from tests.models import RESTFrameworkModel
# Models
@@ -31,7 +31,7 @@ class AssociatedModelSerializer(serializers.ModelSerializer):
# Tests
-class IneritedModelSerializationTests(TestCase):
+class InheritedModelSerializationTests(TestCase):
def test_multitable_inherited_model_fields_as_expected(self):
"""
@@ -48,8 +48,8 @@ class IneritedModelSerializationTests(TestCase):
Assert that a model with a onetoone field that is the primary key is
not treated like a derived model
"""
- parent = ParentModel(name1='parent name')
- associate = AssociatedModel(name='hello', ref=parent)
+ parent = ParentModel.objects.create(name1='parent name')
+ associate = AssociatedModel.objects.create(name='hello', ref=parent)
serializer = AssociatedModelSerializer(associate)
self.assertEqual(set(serializer.data.keys()),
set(['name', 'ref']))
diff --git a/rest_framework/tests/test_negotiation.py b/tests/test_negotiation.py
similarity index 100%
rename from rest_framework/tests/test_negotiation.py
rename to tests/test_negotiation.py
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 000000000..6b39a6f22
--- /dev/null
+++ b/tests/test_pagination.py
@@ -0,0 +1,671 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from rest_framework import exceptions, generics, pagination, serializers, status, filters
+from rest_framework.request import Request
+from rest_framework.pagination import PageLink, PAGE_BREAK
+from rest_framework.test import APIRequestFactory
+import pytest
+
+factory = APIRequestFactory()
+
+
+class TestPaginationIntegration:
+ """
+ Integration tests.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ class EvenItemsOnly(filters.BaseFilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ return [item for item in queryset if item % 2 == 0]
+
+ class BasicPagination(pagination.PageNumberPagination):
+ page_size = 5
+ page_size_query_param = 'page_size'
+ max_page_size = 20
+
+ self.view = generics.ListAPIView.as_view(
+ serializer_class=PassThroughSerializer,
+ queryset=range(1, 101),
+ filter_backends=[EvenItemsOnly],
+ pagination_class=BasicPagination
+ )
+
+ def test_filtered_items_are_paginated(self):
+ request = factory.get('/', {'page': 2})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [12, 14, 16, 18, 20],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page=3',
+ 'count': 50
+ }
+
+ def test_setting_page_size(self):
+ """
+ When 'paginate_by_param' is set, the client may choose a page size.
+ """
+ request = factory.get('/', {'page_size': 10})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=10',
+ 'count': 50
+ }
+
+ def test_setting_page_size_over_maximum(self):
+ """
+ When page_size parameter exceeds maxiumum allowable,
+ then it should be capped to the maxiumum.
+ """
+ request = factory.get('/', {'page_size': 1000})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [
+ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
+ 22, 24, 26, 28, 30, 32, 34, 36, 38, 40
+ ],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=1000',
+ 'count': 50
+ }
+
+ def test_setting_page_size_to_zero(self):
+ """
+ When page_size parameter is invalid it should return to the default.
+ """
+ request = factory.get('/', {'page_size': 0})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [2, 4, 6, 8, 10],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=0',
+ 'count': 50
+ }
+
+ def test_additional_query_params_are_preserved(self):
+ request = factory.get('/', {'page': 2, 'filter': 'even'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [12, 14, 16, 18, 20],
+ 'previous': 'http://testserver/?filter=even',
+ 'next': 'http://testserver/?filter=even&page=3',
+ 'count': 50
+ }
+
+ def test_404_not_found_for_zero_page(self):
+ request = factory.get('/', {'page': '0'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert response.data == {
+ 'detail': 'Invalid page "0": That page number is less than 1.'
+ }
+
+ def test_404_not_found_for_invalid_page(self):
+ request = factory.get('/', {'page': 'invalid'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert response.data == {
+ 'detail': 'Invalid page "invalid": That page number is not an integer.'
+ }
+
+
+class TestPaginationDisabledIntegration:
+ """
+ Integration tests for disabled pagination.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ self.view = generics.ListAPIView.as_view(
+ serializer_class=PassThroughSerializer,
+ queryset=range(1, 101),
+ pagination_class=None
+ )
+
+ def test_unpaginated_list(self):
+ request = factory.get('/', {'page': 2})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == list(range(1, 101))
+
+
+class TestDeprecatedStylePagination:
+ """
+ Integration tests for deprecated style of setting pagination
+ attributes on the view.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ class ExampleView(generics.ListAPIView):
+ serializer_class = PassThroughSerializer
+ queryset = range(1, 101)
+ pagination_class = pagination.PageNumberPagination
+ paginate_by = 20
+ page_query_param = 'page_number'
+
+ self.view = ExampleView.as_view()
+
+ def test_paginate_by_attribute_on_view(self):
+ request = factory.get('/?page_number=2')
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
+ ],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page_number=3',
+ 'count': 100
+ }
+
+
+class TestPageNumberPagination:
+ """
+ Unit tests for `pagination.PageNumberPagination`.
+ """
+
+ def setup(self):
+ class ExamplePagination(pagination.PageNumberPagination):
+ page_size = 5
+ self.pagination = ExamplePagination()
+ self.queryset = range(1, 101)
+
+ def paginate_queryset(self, request):
+ return list(self.pagination.paginate_queryset(self.queryset, request))
+
+ def get_paginated_content(self, queryset):
+ response = self.pagination.get_paginated_response(queryset)
+ return response.data
+
+ def get_html_context(self):
+ return self.pagination.get_html_context()
+
+ def test_no_page_number(self):
+ request = Request(factory.get('/'))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [1, 2, 3, 4, 5]
+ assert content == {
+ 'results': [1, 2, 3, 4, 5],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': None,
+ 'next_url': 'http://testserver/?page=2',
+ 'page_links': [
+ PageLink('http://testserver/', 1, True, False),
+ PageLink('http://testserver/?page=2', 2, False, False),
+ PageLink('http://testserver/?page=3', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=20', 20, False, False),
+ ]
+ }
+ assert self.pagination.display_page_controls
+ assert isinstance(self.pagination.to_html(), type(''))
+
+ def test_second_page(self):
+ request = Request(factory.get('/', {'page': 2}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [6, 7, 8, 9, 10]
+ assert content == {
+ 'results': [6, 7, 8, 9, 10],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page=3',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/',
+ 'next_url': 'http://testserver/?page=3',
+ 'page_links': [
+ PageLink('http://testserver/', 1, False, False),
+ PageLink('http://testserver/?page=2', 2, True, False),
+ PageLink('http://testserver/?page=3', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=20', 20, False, False),
+ ]
+ }
+
+ def test_last_page(self):
+ request = Request(factory.get('/', {'page': 'last'}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [96, 97, 98, 99, 100]
+ assert content == {
+ 'results': [96, 97, 98, 99, 100],
+ 'previous': 'http://testserver/?page=19',
+ 'next': None,
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?page=19',
+ 'next_url': None,
+ 'page_links': [
+ PageLink('http://testserver/', 1, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=18', 18, False, False),
+ PageLink('http://testserver/?page=19', 19, False, False),
+ PageLink('http://testserver/?page=20', 20, True, False),
+ ]
+ }
+
+ def test_invalid_page(self):
+ request = Request(factory.get('/', {'page': 'invalid'}))
+ with pytest.raises(exceptions.NotFound):
+ self.paginate_queryset(request)
+
+
+class TestLimitOffset:
+ """
+ Unit tests for `pagination.LimitOffsetPagination`.
+ """
+
+ def setup(self):
+ class ExamplePagination(pagination.LimitOffsetPagination):
+ default_limit = 10
+ self.pagination = ExamplePagination()
+ self.queryset = range(1, 101)
+
+ def paginate_queryset(self, request):
+ return list(self.pagination.paginate_queryset(self.queryset, request))
+
+ def get_paginated_content(self, queryset):
+ response = self.pagination.get_paginated_response(queryset)
+ return response.data
+
+ def get_html_context(self):
+ return self.pagination.get_html_context()
+
+ def test_no_offset(self):
+ request = Request(factory.get('/', {'limit': 5}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [1, 2, 3, 4, 5]
+ assert content == {
+ 'results': [1, 2, 3, 4, 5],
+ 'previous': None,
+ 'next': 'http://testserver/?limit=5&offset=5',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': None,
+ 'next_url': 'http://testserver/?limit=5&offset=5',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, True, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, False, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+ assert self.pagination.display_page_controls
+ assert isinstance(self.pagination.to_html(), type(''))
+
+ def test_single_offset(self):
+ """
+ When the offset is not a multiple of the limit we get some edge cases:
+ * The first page should still be offset zero.
+ * We may end up displaying an extra page in the pagination control.
+ """
+ request = Request(factory.get('/', {'limit': 5, 'offset': 1}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [2, 3, 4, 5, 6]
+ assert content == {
+ 'results': [2, 3, 4, 5, 6],
+ 'previous': 'http://testserver/?limit=5',
+ 'next': 'http://testserver/?limit=5&offset=6',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5',
+ 'next_url': 'http://testserver/?limit=5&offset=6',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=1', 2, True, False),
+ PageLink('http://testserver/?limit=5&offset=6', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=96', 21, False, False),
+ ]
+ }
+
+ def test_first_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 5}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [6, 7, 8, 9, 10]
+ assert content == {
+ 'results': [6, 7, 8, 9, 10],
+ 'previous': 'http://testserver/?limit=5',
+ 'next': 'http://testserver/?limit=5&offset=10',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5',
+ 'next_url': 'http://testserver/?limit=5&offset=10',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, True, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+
+ def test_middle_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 10}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [11, 12, 13, 14, 15]
+ assert content == {
+ 'results': [11, 12, 13, 14, 15],
+ 'previous': 'http://testserver/?limit=5&offset=5',
+ 'next': 'http://testserver/?limit=5&offset=15',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5&offset=5',
+ 'next_url': 'http://testserver/?limit=5&offset=15',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, False, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, True, False),
+ PageLink('http://testserver/?limit=5&offset=15', 4, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+
+ def test_ending_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 95}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [96, 97, 98, 99, 100]
+ assert content == {
+ 'results': [96, 97, 98, 99, 100],
+ 'previous': 'http://testserver/?limit=5&offset=90',
+ 'next': None,
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5&offset=90',
+ 'next_url': None,
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=85', 18, False, False),
+ PageLink('http://testserver/?limit=5&offset=90', 19, False, False),
+ PageLink('http://testserver/?limit=5&offset=95', 20, True, False),
+ ]
+ }
+
+ def test_invalid_offset(self):
+ """
+ An invalid offset query param should be treated as 0.
+ """
+ request = Request(factory.get('/', {'limit': 5, 'offset': 'invalid'}))
+ queryset = self.paginate_queryset(request)
+ assert queryset == [1, 2, 3, 4, 5]
+
+ def test_invalid_limit(self):
+ """
+ An invalid limit query param should be ignored in favor of the default.
+ """
+ request = Request(factory.get('/', {'limit': 'invalid', 'offset': 0}))
+ queryset = self.paginate_queryset(request)
+ assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+
+class TestCursorPagination:
+ """
+ Unit tests for `pagination.CursorPagination`.
+ """
+
+ def setup(self):
+ class MockObject(object):
+ def __init__(self, idx):
+ self.created = idx
+
+ class MockQuerySet(object):
+ def __init__(self, items):
+ self.items = items
+
+ def filter(self, created__gt=None, created__lt=None):
+ if created__gt is not None:
+ return MockQuerySet([
+ item for item in self.items
+ if item.created > int(created__gt)
+ ])
+
+ assert created__lt is not None
+ return MockQuerySet([
+ item for item in self.items
+ if item.created < int(created__lt)
+ ])
+
+ def order_by(self, *ordering):
+ if ordering[0].startswith('-'):
+ return MockQuerySet(list(reversed(self.items)))
+ return self
+
+ def __getitem__(self, sliced):
+ return self.items[sliced]
+
+ class ExamplePagination(pagination.CursorPagination):
+ page_size = 5
+ ordering = 'created'
+
+ self.pagination = ExamplePagination()
+ self.queryset = MockQuerySet([
+ MockObject(idx) for idx in [
+ 1, 1, 1, 1, 1,
+ 1, 2, 3, 4, 4,
+ 4, 4, 5, 6, 7,
+ 7, 7, 7, 7, 7,
+ 7, 7, 7, 8, 9,
+ 9, 9, 9, 9, 9
+ ]
+ ])
+
+ def get_pages(self, url):
+ """
+ Given a URL return a tuple of:
+
+ (previous page, current page, next page, previous url, next url)
+ """
+ request = Request(factory.get(url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ current = [item.created for item in queryset]
+
+ next_url = self.pagination.get_next_link()
+ previous_url = self.pagination.get_previous_link()
+
+ if next_url is not None:
+ request = Request(factory.get(next_url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ next = [item.created for item in queryset]
+ else:
+ next = None
+
+ if previous_url is not None:
+ request = Request(factory.get(previous_url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ previous = [item.created for item in queryset]
+ else:
+ previous = None
+
+ return (previous, current, next, previous_url, next_url)
+
+ def test_invalid_cursor(self):
+ request = Request(factory.get('/', {'cursor': '123'}))
+ with pytest.raises(exceptions.NotFound):
+ self.pagination.paginate_queryset(self.queryset, request)
+
+ def test_use_with_ordering_filter(self):
+ class MockView:
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = ['username', 'created']
+ ordering = 'created'
+
+ request = Request(factory.get('/', {'ordering': 'username'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('username',)
+
+ request = Request(factory.get('/', {'ordering': '-username'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('-username',)
+
+ request = Request(factory.get('/', {'ordering': 'invalid'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('created',)
+
+ def test_cursor_pagination(self):
+ (previous, current, next, previous_url, next_url) = self.get_pages('/')
+
+ assert previous is None
+ assert current == [1, 1, 1, 1, 1]
+ assert next == [1, 2, 3, 4, 4]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [1, 1, 1, 1, 1]
+ assert current == [1, 2, 3, 4, 4]
+ assert next == [4, 4, 5, 6, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [1, 2, 3, 4, 4]
+ assert current == [4, 4, 5, 6, 7]
+ assert next == [7, 7, 7, 7, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [4, 4, 4, 5, 6] # Paging artifact
+ assert current == [7, 7, 7, 7, 7]
+ assert next == [7, 7, 7, 8, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [7, 7, 7, 7, 7]
+ assert current == [7, 7, 7, 8, 9]
+ assert next == [9, 9, 9, 9, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [7, 7, 7, 8, 9]
+ assert current == [9, 9, 9, 9, 9]
+ assert next is None
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [7, 7, 7, 7, 7]
+ assert current == [7, 7, 7, 8, 9]
+ assert next == [9, 9, 9, 9, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [4, 4, 5, 6, 7]
+ assert current == [7, 7, 7, 7, 7]
+ assert next == [8, 9, 9, 9, 9] # Paging artifact
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [1, 2, 3, 4, 4]
+ assert current == [4, 4, 5, 6, 7]
+ assert next == [7, 7, 7, 7, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [1, 1, 1, 1, 1]
+ assert current == [1, 2, 3, 4, 4]
+ assert next == [4, 4, 5, 6, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous is None
+ assert current == [1, 1, 1, 1, 1]
+ assert next == [1, 2, 3, 4, 4]
+
+ assert isinstance(self.pagination.to_html(), type(''))
+
+
+def test_get_displayed_page_numbers():
+ """
+ Test our contextual page display function.
+
+ This determines which pages to display in a pagination control,
+ given the current page and the last page.
+ """
+ displayed_page_numbers = pagination._get_displayed_page_numbers
+
+ # At five pages or less, all pages are displayed, always.
+ assert displayed_page_numbers(1, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(2, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(3, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(4, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(5, 5) == [1, 2, 3, 4, 5]
+
+ # Between six and either pages we may have a single page break.
+ assert displayed_page_numbers(1, 6) == [1, 2, 3, None, 6]
+ assert displayed_page_numbers(2, 6) == [1, 2, 3, None, 6]
+ assert displayed_page_numbers(3, 6) == [1, 2, 3, 4, 5, 6]
+ assert displayed_page_numbers(4, 6) == [1, 2, 3, 4, 5, 6]
+ assert displayed_page_numbers(5, 6) == [1, None, 4, 5, 6]
+ assert displayed_page_numbers(6, 6) == [1, None, 4, 5, 6]
+
+ assert displayed_page_numbers(1, 7) == [1, 2, 3, None, 7]
+ assert displayed_page_numbers(2, 7) == [1, 2, 3, None, 7]
+ assert displayed_page_numbers(3, 7) == [1, 2, 3, 4, None, 7]
+ assert displayed_page_numbers(4, 7) == [1, 2, 3, 4, 5, 6, 7]
+ assert displayed_page_numbers(5, 7) == [1, None, 4, 5, 6, 7]
+ assert displayed_page_numbers(6, 7) == [1, None, 5, 6, 7]
+ assert displayed_page_numbers(7, 7) == [1, None, 5, 6, 7]
+
+ assert displayed_page_numbers(1, 8) == [1, 2, 3, None, 8]
+ assert displayed_page_numbers(2, 8) == [1, 2, 3, None, 8]
+ assert displayed_page_numbers(3, 8) == [1, 2, 3, 4, None, 8]
+ assert displayed_page_numbers(4, 8) == [1, 2, 3, 4, 5, None, 8]
+ assert displayed_page_numbers(5, 8) == [1, None, 4, 5, 6, 7, 8]
+ assert displayed_page_numbers(6, 8) == [1, None, 5, 6, 7, 8]
+ assert displayed_page_numbers(7, 8) == [1, None, 6, 7, 8]
+ assert displayed_page_numbers(8, 8) == [1, None, 6, 7, 8]
+
+ # At nine or more pages we may have two page breaks, one on each side.
+ assert displayed_page_numbers(1, 9) == [1, 2, 3, None, 9]
+ assert displayed_page_numbers(2, 9) == [1, 2, 3, None, 9]
+ assert displayed_page_numbers(3, 9) == [1, 2, 3, 4, None, 9]
+ assert displayed_page_numbers(4, 9) == [1, 2, 3, 4, 5, None, 9]
+ assert displayed_page_numbers(5, 9) == [1, None, 4, 5, 6, None, 9]
+ assert displayed_page_numbers(6, 9) == [1, None, 5, 6, 7, 8, 9]
+ assert displayed_page_numbers(7, 9) == [1, None, 6, 7, 8, 9]
+ assert displayed_page_numbers(8, 9) == [1, None, 7, 8, 9]
+ assert displayed_page_numbers(9, 9) == [1, None, 7, 8, 9]
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
new file mode 100644
index 000000000..fe6aec196
--- /dev/null
+++ b/tests/test_parsers.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+from django import forms
+from django.core.files.uploadhandler import MemoryFileUploadHandler
+from django.test import TestCase
+from django.utils.six.moves import StringIO
+from rest_framework.exceptions import ParseError
+from rest_framework.parsers import FormParser, FileUploadParser
+
+
+class Form(forms.Form):
+ field1 = forms.CharField(max_length=3)
+ field2 = forms.CharField()
+
+
+class TestFormParser(TestCase):
+ def setUp(self):
+ self.string = "field1=abc&field2=defghijk"
+
+ def test_parse(self):
+ """ Make sure the `QueryDict` works OK """
+ parser = FormParser()
+
+ stream = StringIO(self.string)
+ data = parser.parse(stream)
+
+ self.assertEqual(Form(data).is_valid(), True)
+
+
+class TestFileUploadParser(TestCase):
+ def setUp(self):
+ class MockRequest(object):
+ pass
+ from io import BytesIO
+ self.stream = BytesIO(
+ "Test text file".encode('utf-8')
+ )
+ request = MockRequest()
+ request.upload_handlers = (MemoryFileUploadHandler(),)
+ request.META = {
+ 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt',
+ 'HTTP_CONTENT_LENGTH': 14,
+ }
+ self.parser_context = {'request': request, 'kwargs': {}}
+
+ def test_parse(self):
+ """
+ Parse raw file upload.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ data_and_files = parser.parse(self.stream, None, self.parser_context)
+ file_obj = data_and_files.files['file']
+ self.assertEqual(file_obj._size, 14)
+
+ def test_parse_missing_filename(self):
+ """
+ Parse raw file upload when filename is missing.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
+ with self.assertRaises(ParseError):
+ parser.parse(self.stream, None, self.parser_context)
+
+ def test_parse_missing_filename_multiple_upload_handlers(self):
+ """
+ Parse raw file upload with multiple handlers when filename is missing.
+ Regression test for #2109.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ self.parser_context['request'].upload_handlers = (
+ MemoryFileUploadHandler(),
+ MemoryFileUploadHandler()
+ )
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
+ with self.assertRaises(ParseError):
+ parser.parse(self.stream, None, self.parser_context)
+
+ def test_get_filename(self):
+ parser = FileUploadParser()
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'file.txt')
+
+ def test_get_encoded_filename(self):
+ parser = FileUploadParser()
+
+ self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ def __replace_content_disposition(self, disposition):
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
new file mode 100644
index 000000000..97bac33db
--- /dev/null
+++ b/tests/test_permissions.py
@@ -0,0 +1,312 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User, Permission, Group
+from django.db import models
+from django.test import TestCase
+from django.utils import unittest
+from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
+from rest_framework.compat import guardian, get_model_name
+from rest_framework.filters import DjangoObjectPermissionsFilter
+from rest_framework.test import APIRequestFactory
+from tests.models import BasicModel
+import base64
+
+factory = APIRequestFactory()
+
+
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class RootView(generics.ListCreateAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [permissions.DjangoModelPermissions]
+
+
+class InstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [permissions.DjangoModelPermissions]
+
+root_view = RootView.as_view()
+instance_view = InstanceView.as_view()
+
+
+def basic_auth_header(username, password):
+ credentials = ('%s:%s' % (username, password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ return 'Basic %s' % base64_credentials
+
+
+class ModelPermissionsIntegrationTests(TestCase):
+ def setUp(self):
+ User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
+ user = User.objects.create_user('permitted', 'permitted@example.com', 'password')
+ user.user_permissions = [
+ Permission.objects.get(codename='add_basicmodel'),
+ Permission.objects.get(codename='change_basicmodel'),
+ Permission.objects.get(codename='delete_basicmodel')
+ ]
+ user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
+ user.user_permissions = [
+ Permission.objects.get(codename='change_basicmodel'),
+ ]
+
+ self.permitted_credentials = basic_auth_header('permitted', 'password')
+ self.disallowed_credentials = basic_auth_header('disallowed', 'password')
+ self.updateonly_credentials = basic_auth_header('updateonly', 'password')
+
+ BasicModel(text='foo').save()
+
+ def test_has_create_permissions(self):
+ request = factory.post('/', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = root_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_has_put_permissions(self):
+ request = factory.put('/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_has_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = instance_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_does_not_have_create_permissions(self):
+ request = factory.post('/', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = root_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_does_not_have_put_permissions(self):
+ request = factory.put('/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_does_not_have_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = instance_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_options_permitted(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['POST'])
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+
+ def test_options_disallowed(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ def test_options_updateonly(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+
+
+class BasicPermModel(models.Model):
+ text = models.CharField(max_length=100)
+
+ class Meta:
+ app_label = 'tests'
+ permissions = (
+ ('view_basicpermmodel', 'Can view basic perm model'),
+ # add, change, delete built in to django
+ )
+
+
+class BasicPermSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicPermModel
+
+
+# Custom object-level permission, that includes 'view' permissions
+class ViewObjectPermissions(permissions.DjangoObjectPermissions):
+ perms_map = {
+ 'GET': ['%(app_label)s.view_%(model_name)s'],
+ 'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
+ 'HEAD': ['%(app_label)s.view_%(model_name)s'],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
+
+
+class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [ViewObjectPermissions]
+
+object_permissions_view = ObjectPermissionInstanceView.as_view()
+
+
+class ObjectPermissionListView(generics.ListAPIView):
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [ViewObjectPermissions]
+
+object_permissions_list_view = ObjectPermissionListView.as_view()
+
+
+@unittest.skipUnless(guardian, 'django-guardian not installed')
+class ObjectPermissionsIntegrationTests(TestCase):
+ """
+ Integration tests for the object level permissions API.
+ """
+ def setUp(self):
+ from guardian.shortcuts import assign_perm
+
+ # create users
+ create = User.objects.create_user
+ users = {
+ 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'),
+ 'readonly': create('readonly', 'readonly@example.com', 'password'),
+ 'writeonly': create('writeonly', 'writeonly@example.com', 'password'),
+ 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'),
+ }
+
+ # give everyone model level permissions, as we are not testing those
+ everyone = Group.objects.create(name='everyone')
+ model_name = get_model_name(BasicPermModel)
+ app_label = BasicPermModel._meta.app_label
+ f = '{0}_{1}'.format
+ perms = {
+ 'view': f('view', model_name),
+ 'change': f('change', model_name),
+ 'delete': f('delete', model_name)
+ }
+ for perm in perms.values():
+ perm = '{0}.{1}'.format(app_label, perm)
+ assign_perm(perm, everyone)
+ everyone.user_set.add(*users.values())
+
+ # appropriate object level permissions
+ readers = Group.objects.create(name='readers')
+ writers = Group.objects.create(name='writers')
+ deleters = Group.objects.create(name='deleters')
+
+ model = BasicPermModel.objects.create(text='foo')
+
+ assign_perm(perms['view'], readers, model)
+ assign_perm(perms['change'], writers, model)
+ assign_perm(perms['delete'], deleters, model)
+
+ readers.user_set.add(users['fullaccess'], users['readonly'])
+ writers.user_set.add(users['fullaccess'], users['writeonly'])
+ deleters.user_set.add(users['fullaccess'], users['deleteonly'])
+
+ self.credentials = {}
+ for user in users.values():
+ self.credentials[user.username] = basic_auth_header(user.username, 'password')
+
+ # Delete
+ def test_can_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_cannot_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ # Update
+ def test_can_update_permissions(self):
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['writeonly']
+ )
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data.get('text'), 'foobar')
+
+ def test_cannot_update_permissions(self):
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_cannot_update_permissions_non_existing(self):
+ request = factory.patch(
+ '/999', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
+ response = object_permissions_view(request, pk='999')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Read
+ def test_can_read_permissions(self):
+ request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_cannot_read_permissions(self):
+ request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Read list
+ def test_can_read_list_permissions(self):
+ request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
+ response = object_permissions_list_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data[0].get('id'), 1)
+
+ def test_cannot_read_list_permissions(self):
+ request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly'])
+ object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
+ response = object_permissions_list_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertListEqual(response.data, [])
diff --git a/tests/test_relations.py b/tests/test_relations.py
new file mode 100644
index 000000000..fbe176e24
--- /dev/null
+++ b/tests/test_relations.py
@@ -0,0 +1,169 @@
+from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import MultiValueDict
+from rest_framework import serializers
+from rest_framework.fields import empty
+from rest_framework.test import APISimpleTestCase
+import pytest
+
+
+class TestStringRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.StringRelatedField()
+
+ def test_string_related_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == ''
+
+
+class TestPrimaryKeyRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.PrimaryKeyRelatedField(queryset=self.queryset)
+
+ def test_pk_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.pk)
+ assert instance is self.instance
+
+ def test_pk_related_lookup_does_not_exist(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(4)
+ msg = excinfo.value.detail[0]
+ assert msg == 'Invalid pk "4" - object does not exist.'
+
+ def test_pk_related_lookup_invalid_type(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.detail[0]
+ assert msg == 'Incorrect type. Expected pk value, received BadType.'
+
+ def test_pk_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.pk
+
+
+class TestHyperlinkedIdentityField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example')
+ self.field.reverse = mock_reverse
+ self.field._context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_unsaved_object(self):
+ representation = self.field.to_representation(MockObject(pk=None))
+ assert representation is None
+
+ def test_representation_with_format(self):
+ self.field._context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.xml/'
+
+ def test_improperly_configured(self):
+ """
+ If a matching view cannot be reversed with the given instance,
+ the the user has misconfigured something, as the URL conf and the
+ hyperlinked field do not match.
+ """
+ self.field.reverse = fail_reverse
+ with pytest.raises(ImproperlyConfigured):
+ self.field.to_representation(self.instance)
+
+
+class TestHyperlinkedIdentityFieldWithFormat(APISimpleTestCase):
+ """
+ Tests for a hyperlinked identity field that has a `format` set,
+ which enforces that alternate formats are never linked too.
+
+ Eg. If your API includes some endpoints that accept both `.xml` and `.json`,
+ but other endpoints that only accept `.json`, we allow for hyperlinked
+ relationships that enforce only a single suffix type.
+ """
+
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example', format='json')
+ self.field.reverse = mock_reverse
+ self.field._context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_with_format(self):
+ self.field._context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.json/'
+
+
+class TestSlugRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.SlugRelatedField(
+ slug_field='name', queryset=self.queryset
+ )
+
+ def test_slug_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.name)
+ assert instance is self.instance
+
+ def test_slug_related_lookup_does_not_exist(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value('doesnotexist')
+ msg = excinfo.value.detail[0]
+ assert msg == 'Object with name=doesnotexist does not exist.'
+
+ def test_slug_related_lookup_invalid_type(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.detail[0]
+ assert msg == 'Invalid value.'
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.name
+
+
+class TestManyRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.StringRelatedField(many=True)
+ self.field.field_name = 'foo'
+
+ def test_get_value_regular_dictionary_full(self):
+ assert 'bar' == self.field.get_value({'foo': 'bar'})
+ assert empty == self.field.get_value({'baz': 'bar'})
+
+ def test_get_value_regular_dictionary_partial(self):
+ setattr(self.field.root, 'partial', True)
+ assert 'bar' == self.field.get_value({'foo': 'bar'})
+ assert empty == self.field.get_value({'baz': 'bar'})
+
+ def test_get_value_multi_dictionary_full(self):
+ mvd = MultiValueDict({'foo': ['bar1', 'bar2']})
+ assert ['bar1', 'bar2'] == self.field.get_value(mvd)
+
+ mvd = MultiValueDict({'baz': ['bar1', 'bar2']})
+ assert [] == self.field.get_value(mvd)
+
+ def test_get_value_multi_dictionary_partial(self):
+ setattr(self.field.root, 'partial', True)
+ mvd = MultiValueDict({'foo': ['bar1', 'bar2']})
+ assert ['bar1', 'bar2'] == self.field.get_value(mvd)
+
+ mvd = MultiValueDict({'baz': ['bar1', 'bar2']})
+ assert empty == self.field.get_value(mvd)
diff --git a/rest_framework/tests/test_genericrelations.py b/tests/test_relations_generic.py
similarity index 74%
rename from rest_framework/tests/test_genericrelations.py
rename to tests/test_relations_generic.py
index c38bfb9f3..b600b3333 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/tests/test_relations_generic.py
@@ -3,9 +3,11 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.db import models
from django.test import TestCase
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import serializers
+@python_2_unicode_compatible
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
@@ -15,10 +17,11 @@ class Tag(models.Model):
object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id')
- def __unicode__(self):
+ def __str__(self):
return self.tag
+@python_2_unicode_compatible
class Bookmark(models.Model):
"""
A URL bookmark that may have multiple tags attached.
@@ -26,10 +29,11 @@ class Bookmark(models.Model):
url = models.URLField()
tags = GenericRelation(Tag)
- def __unicode__(self):
+ def __str__(self):
return 'Bookmark: %s' % self.url
+@python_2_unicode_compatible
class Note(models.Model):
"""
A textual note that may have multiple tags attached.
@@ -37,7 +41,7 @@ class Note(models.Model):
text = models.TextField()
tags = GenericRelation(Tag)
- def __unicode__(self):
+ def __str__(self):
return 'Note: %s' % self.text
@@ -56,11 +60,11 @@ class TestGenericRelations(TestCase):
"""
class BookmarkSerializer(serializers.ModelSerializer):
- tags = serializers.RelatedField(many=True)
+ tags = serializers.StringRelatedField(many=True)
class Meta:
model = Bookmark
- exclude = ('id',)
+ fields = ('tags', 'url')
serializer = BookmarkSerializer(self.bookmark)
expected = {
@@ -76,25 +80,25 @@ class TestGenericRelations(TestCase):
"""
class TagSerializer(serializers.ModelSerializer):
- tagged_item = serializers.RelatedField()
+ tagged_item = serializers.StringRelatedField()
class Meta:
model = Tag
- exclude = ('id', 'content_type', 'object_id')
+ fields = ('tag', 'tagged_item')
serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [
- {
- 'tag': 'django',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'python',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'reminder',
- 'tagged_item': 'Note: Remember the milk'
- }
+ {
+ 'tag': 'django',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'python',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'reminder',
+ 'tagged_item': 'Note: Remember the milk'
+ }
]
self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
similarity index 82%
rename from rest_framework/tests/test_relations_hyperlink.py
rename to tests/test_relations_hyperlink.py
index 3c4d39af6..33b09713a 100644
--- a/rest_framework/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -1,10 +1,9 @@
from __future__ import unicode_literals
+from django.conf.urls import url
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.compat import patterns, url
from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import (
- BlogPost,
+from tests.models import (
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
)
@@ -16,7 +15,8 @@ request = factory.get('/') # Just to ensure we have a request in the serializer
def dummy_view(request, pk):
pass
-urlpatterns = patterns('',
+
+urlpatterns = [
url(r'^dummyurl/(?P[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
@@ -25,7 +25,7 @@ urlpatterns = patterns('',
url(r'^nullableforeignkeysource/(?P[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
url(r'^onetoonetarget/(?P[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
url(r'^nullableonetoonesource/(?P[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
-)
+]
# ManyToMany
@@ -71,7 +71,7 @@ class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
# TODO: Add test that .data cannot be accessed prior to .is_valid
class HyperlinkedManyToManyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
for idx in range(1, 4):
@@ -86,11 +86,18 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_many_to_many_retrieve_prefetch_related(self):
+ queryset = ManyToManySource.objects.all().prefetch_related('targets')
+ serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
+ with self.assertNumQueries(2):
+ serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
@@ -100,7 +107,8 @@ class HyperlinkedManyToManyTests(TestCase):
{'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
def test_many_to_many_update(self):
data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
@@ -114,9 +122,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
self.assertEqual(serializer.data, expected)
@@ -179,7 +187,7 @@ class HyperlinkedManyToManyTests(TestCase):
class HyperlinkedForeignKeyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
@@ -198,7 +206,8 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(1):
+ self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -207,15 +216,16 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(3):
+ self.assertEqual(serializer.data, expected)
def test_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -232,7 +242,7 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']})
+ self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
def test_reverse_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
@@ -303,11 +313,11 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
class HyperlinkedNullableForeignKeyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
@@ -376,8 +386,8 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -399,8 +409,8 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -412,30 +422,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
class HyperlinkedNullableOneToOneTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = OneToOneTarget(name='target-1')
@@ -453,72 +442,3 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
]
self.assertEqual(serializer.data, expected)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class HyperlinkedRelatedFieldSourceTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
-
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_manager',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_queryset',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='a.b.c',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
diff --git a/rest_framework/tests/test_relations_pk.py b/tests/test_relations_pk.py
similarity index 73%
rename from rest_framework/tests/test_relations_pk.py
rename to tests/test_relations_pk.py
index e2a1b8152..ca43272b0 100644
--- a/rest_framework/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -1,12 +1,11 @@
from __future__ import unicode_literals
-from django.db import models
from django.test import TestCase
+from django.utils import six
from rest_framework import serializers
-from rest_framework.tests.models import (
- BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
+from tests.models import (
+ ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
)
-from rest_framework.compat import six
# ManyToMany
@@ -65,11 +64,18 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_many_to_many_retrieve_prefetch_related(self):
+ queryset = ManyToManySource.objects.all().prefetch_related('targets')
+ serializer = ManyToManySourceSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
@@ -79,7 +85,8 @@ class PKManyToManyTests(TestCase):
{'id': 2, 'name': 'target-2', 'sources': [2, 3]},
{'id': 3, 'name': 'target-3', 'sources': [3]}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
def test_many_to_many_update(self):
data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}
@@ -93,9 +100,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
self.assertEqual(serializer.data, expected)
@@ -128,7 +135,6 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
- self.assertFalse(serializer.fields['targets'].read_only)
expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
@@ -137,10 +143,19 @@ class PKManyToManyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
+ def test_many_to_many_unsaved(self):
+ source = ManyToManySource(name='source-unsaved')
+
+ serializer = ManyToManySourceSerializer(source)
+
+ expected = {'id': None, 'name': 'source-unsaved', 'targets': []}
+ # no query if source hasn't been created yet
+ with self.assertNumQueries(0):
+ self.assertEqual(serializer.data, expected)
+
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
- self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(serializer.data, data)
@@ -176,7 +191,8 @@ class PKForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 1},
{'id': 3, 'name': 'source-3', 'target': 1}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(1):
+ self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -185,15 +201,22 @@ class PKForeignKeyTests(TestCase):
{'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': 'target-2', 'sources': []},
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(3):
+ self.assertEqual(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve_prefetch_related(self):
+ queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
+ serializer = ForeignKeyTargetSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 2}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -210,7 +233,7 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
+ self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
def test_reverse_foreign_key_update(self):
data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
@@ -281,7 +304,26 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
+
+ def test_foreign_key_with_unsaved(self):
+ source = ForeignKeySource(name='source-unsaved')
+ expected = {'id': None, 'name': 'source-unsaved', 'target': None}
+
+ serializer = ForeignKeySourceSerializer(source)
+
+ # no query if source hasn't been created yet
+ with self.assertNumQueries(0):
+ self.assertEqual(serializer.data, expected)
+
+ def test_foreign_key_with_empty(self):
+ """
+ Regression test for #1072
+
+ https://github.com/tomchristie/django-rest-framework/issues/1072
+ """
+ serializer = NullableForeignKeySourceSerializer()
+ self.assertEqual(serializer.data['target'], None)
class PKNullableForeignKeyTests(TestCase):
@@ -352,8 +394,8 @@ class PKNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -375,8 +417,8 @@ class PKNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -388,27 +430,6 @@ class PKNullableForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
class PKNullableOneToOneTests(TestCase):
def setUp(self):
@@ -427,116 +448,3 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
self.assertEqual(serializer.data, expected)
-
-
-# The below models and tests ensure that serializer fields corresponding
-# to a ManyToManyField field with a user-specified ``through`` model are
-# set to read only
-
-
-class ManyToManyThroughTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManyThrough(models.Model):
- source = models.ForeignKey('ManyToManyThroughSource')
- target = models.ForeignKey(ManyToManyThroughTarget)
-
-
-class ManyToManyThroughSource(models.Model):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyThroughTarget,
- related_name='sources',
- through='ManyToManyThrough')
-
-
-class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughTarget
- fields = ('id', 'name', 'sources')
-
-
-class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughSource
- fields = ('id', 'name', 'targets')
-
-
-class PKManyToManyThroughTests(TestCase):
- def setUp(self):
- self.source = ManyToManyThroughSource.objects.create(
- name='through-source-1')
- self.target = ManyToManyThroughTarget.objects.create(
- name='through-target-1')
-
- def test_many_to_many_create(self):
- data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
- serializer = ManyToManyThroughSourceSerializer(data=data)
- self.assertTrue(serializer.fields['targets'].read_only)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(obj.name, 'source-2')
- self.assertEqual(obj.targets.count(), 0)
-
- def test_many_to_many_reverse_create(self):
- data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
- serializer = ManyToManyThroughTargetSerializer(data=data)
- self.assertTrue(serializer.fields['sources'].read_only)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- obj = serializer.save()
- self.assertEqual(obj.name, 'target-2')
- self.assertEqual(obj.sources.count(), 0)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-
-class PrimaryKeyRelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
diff --git a/rest_framework/tests/test_relations_slug.py b/tests/test_relations_slug.py
similarity index 90%
rename from rest_framework/tests/test_relations_slug.py
rename to tests/test_relations_slug.py
index 435c821cf..cd2cb1ed6 100644
--- a/rest_framework/tests/test_relations_slug.py
+++ b/tests/test_relations_slug.py
@@ -1,24 +1,35 @@
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
+from tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- sources = serializers.SlugRelatedField(many=True, slug_field='name')
+ sources = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeySource.objects.all(),
+ many=True
+ )
class Meta:
model = ForeignKeyTarget
class ForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name')
+ target = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeyTarget.objects.all()
+ )
class Meta:
model = ForeignKeySource
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name', required=False)
+ target = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeyTarget.objects.all(),
+ allow_null=True
+ )
class Meta:
model = NullableForeignKeySource
@@ -43,7 +54,14 @@ class SlugForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_foreign_key_retrieve_select_related(self):
+ queryset = ForeignKeySource.objects.all().select_related('target')
+ serializer = ForeignKeySourceSerializer(queryset, many=True)
+ with self.assertNumQueries(1):
+ serializer.data
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -54,13 +72,19 @@ class SlugForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
+ def test_reverse_foreign_key_retrieve_prefetch_related(self):
+ queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
+ serializer = ForeignKeyTargetSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
+
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -149,7 +173,7 @@ class SlugForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
class SlugNullableForeignKeyTests(TestCase):
@@ -220,8 +244,8 @@ class SlugNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -243,8 +267,8 @@ class SlugNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
diff --git a/rest_framework/tests/test_renderers.py b/tests/test_renderers.py
similarity index 52%
rename from rest_framework/tests/test_renderers.py
rename to tests/test_renderers.py
index df6f4aa63..cb76f6830 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -1,37 +1,47 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-
-from decimal import Decimal
+from django.conf.urls import patterns, url, include
from django.core.cache import cache
+from django.db import models
from django.test import TestCase
-from django.utils import unittest
+from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
-from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO
+from rest_framework.compat import OrderedDict
from rest_framework.response import Response
from rest_framework.views import APIView
-from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
-from rest_framework.parsers import YAMLParser, XMLParser
+from rest_framework import serializers
+from rest_framework.renderers import (
+ BaseRenderer, JSONRenderer, BrowsableAPIRenderer, HTMLFormRenderer
+)
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
-import datetime
-import pickle
+from collections import MutableMapping
+import json
import re
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+ return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+ return ('Renderer B: %s' % x).encode('ascii')
expected_results = [
- ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator
+ ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1,2,3]') # Generator
]
+class DummyTestModel(models.Model):
+ name = models.CharField(max_length=42, default='')
+
+
class BasicRendererTests(TestCase):
def test_expected_results(self):
for value, renderer_cls, expected in expected_results:
@@ -64,11 +74,22 @@ class MockView(APIView):
class MockGETView(APIView):
-
def get(self, request, **kwargs):
return Response({'foo': ['bar', 'baz']})
+class MockPOSTView(APIView):
+ def post(self, request, **kwargs):
+ return Response({'foo': request.DATA})
+
+
+class EmptyGETView(APIView):
+ renderer_classes = (JSONRenderer,)
+
+ def get(self, request, **kwargs):
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -82,14 +103,15 @@ class HTMLView1(APIView):
def get(self, request, **kwargs):
return Response('text')
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()),
- url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
- url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
+ url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
+ url(r'^empty$', EmptyGETView.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -131,7 +153,7 @@ class RendererEndToEndTests(TestCase):
End-to-end testing of renderers using an RendererMixin on a generic view.
"""
- urls = 'rest_framework.tests.test_renderers'
+ urls = 'tests.test_renderers'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
@@ -219,8 +241,36 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
+ def test_parse_error_renderers_browsable_api(self):
+ """Invalid data should still render the browsable API correctly."""
+ resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
+ self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-_flat_repr = '{"foo": ["bar", "baz"]}'
+ def test_204_no_content_responses_have_no_content_type_set(self):
+ """
+ Regression test for #1196
+
+ https://github.com/tomchristie/django-rest-framework/issues/1196
+ """
+ resp = self.client.get('/empty')
+ self.assertEqual(resp.get('Content-Type', None), None)
+ self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_contains_headers_of_api_response(self):
+ """
+ Issue #1437
+
+ Test we display the headers of the API response and not those from the
+ HTML response
+ """
+ resp = self.client.get('/html1')
+ self.assertContains(resp, '>GET, HEAD, OPTIONS<')
+ self.assertContains(resp, '>application/json<')
+ self.assertNotContains(resp, '>text/html; charset=utf-8<')
+
+
+_flat_repr = '{"foo":["bar","baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -244,6 +294,66 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
+ def test_render_queryset_values(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [{'id': o.id, 'name': o.name}])
+
+ def test_render_queryset_values_list(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values_list('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [[o.id, o.name]])
+
+ def test_render_dict_abc_obj(self):
+ class Dict(MutableMapping):
+ def __init__(self):
+ self._dict = dict()
+
+ def __getitem__(self, key):
+ return self._dict.__getitem__(key)
+
+ def __setitem__(self, key, value):
+ return self._dict.__setitem__(key, value)
+
+ def __delitem__(self, key):
+ return self._dict.__delitem__(key)
+
+ def __iter__(self):
+ return self._dict.__iter__()
+
+ def __len__(self):
+ return self._dict.__len__()
+
+ def keys(self):
+ return self._dict.keys()
+
+ x = Dict()
+ x['key'] = 'string value'
+ x[2] = 3
+ ret = JSONRenderer().render(x)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, {'key': 'string value', '2': 3})
+
+ def test_render_obj_with_getitem(self):
+ class DictLike(object):
+ def __init__(self):
+ self._dict = {}
+
+ def set(self, value):
+ self._dict = dict(value)
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ x = DictLike()
+ x.set({'a': 1, 'b': 'string'})
+ with self.assertRaises(TypeError):
+ JSONRenderer().render(x)
+
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
@@ -263,12 +373,6 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
- def test_check_ascii(self):
- obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = JSONRenderer()
- content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8'))
-
class UnicodeJSONRendererTests(TestCase):
"""
@@ -276,181 +380,31 @@ class UnicodeJSONRendererTests(TestCase):
"""
def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = UnicodeJSONRenderer()
+ renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8'))
+ self.assertEqual(content, '{"countries":["United Kingdom","France","España"]}'.encode('utf-8'))
+
+ def test_u2028_u2029(self):
+ # The \u2028 and \u2029 characters should be escaped,
+ # even when the non-escaping unicode representation is used.
+ # Regression test for #2169
+ obj = {'should_escape': '\u2028\u2029'}
+ renderer = JSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"should_escape":"\\u2028\\u2029"}'.encode('utf-8'))
-class JSONPRendererTests(TestCase):
+class AsciiJSONRendererTests(TestCase):
"""
- Tests specific to the JSONP Renderer
+ Tests specific for the Unicode JSON Renderer
"""
-
- urls = 'rest_framework.tests.test_renderers'
-
- def test_without_callback_with_json_renderer(self):
- """
- Test JSONP rendering with View JSON Renderer.
- """
- resp = self.client.get('/jsonp/jsonrenderer',
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
-
- def test_without_callback_without_json_renderer(self):
- """
- Test JSONP rendering without View JSON Renderer.
- """
- resp = self.client.get('/jsonp/nojsonrenderer',
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
-
- def test_with_callback(self):
- """
- Test JSONP rendering with callback function name.
- """
- callback_func = 'myjsonpcallback'
- resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
-
-
-if yaml:
- _yaml_repr = 'foo: [bar, baz]\n'
-
- class YAMLRendererTests(TestCase):
- """
- Tests specific to the JSON Renderer
- """
-
- def test_render(self):
- """
- Test basic YAML rendering.
- """
- obj = {'foo': ['bar', 'baz']}
- renderer = YAMLRenderer()
- content = renderer.render(obj, 'application/yaml')
- self.assertEqual(content, _yaml_repr)
-
- def test_render_and_parse(self):
- """
- Test rendering and then parsing returns the original object.
- IE obj -> render -> parse -> obj.
- """
- obj = {'foo': ['bar', 'baz']}
-
- renderer = YAMLRenderer()
- parser = YAMLParser()
-
- content = renderer.render(obj, 'application/yaml')
- data = parser.parse(StringIO(content))
- self.assertEqual(obj, data)
-
-
-class XMLRendererTestCase(TestCase):
- """
- Tests specific to the XML Renderer
- """
-
- _complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- def test_render_string(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 'astring'}, 'application/xml')
- self.assertXMLContains(content, 'astring ')
-
- def test_render_integer(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 111}, 'application/xml')
- self.assertXMLContains(content, '111 ')
-
- def test_render_datetime(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({
- 'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }, 'application/xml')
- self.assertXMLContains(content, '2011-12-25 12:45:00 ')
-
- def test_render_float(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 123.4}, 'application/xml')
- self.assertXMLContains(content, '123.4 ')
-
- def test_render_decimal(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
- self.assertXMLContains(content, '111.2 ')
-
- def test_render_none(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': None}, 'application/xml')
- self.assertXMLContains(content, ' ')
-
- def test_render_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render(self._complex_data, 'application/xml')
- self.assertXMLContains(content, 'first ')
- self.assertXMLContains(content, 'second ')
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_render_and_parse_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = StringIO(renderer.render(self._complex_data, 'application/xml'))
-
- parser = XMLParser()
- complex_data_out = parser.parse(content)
- error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
- self.assertEqual(self._complex_data, complex_data_out, error_msg)
-
- def assertXMLContains(self, xml, string):
- self.assertTrue(xml.startswith('\n'))
- self.assertTrue(xml.endswith(' '))
- self.assertTrue(string in xml, '%r not in %r' % (string, xml))
+ def test_proper_encoding(self):
+ class AsciiJSONRenderer(JSONRenderer):
+ ensure_ascii = True
+ obj = {'countries': ['United Kingdom', 'France', 'España']}
+ renderer = AsciiJSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode('utf-8'))
# Tests for caching issue, #346
@@ -459,81 +413,61 @@ class CacheRenderTest(TestCase):
Tests specific to caching responses
"""
- urls = 'rest_framework.tests.test_renderers'
-
- cache_key = 'just_a_cache_key'
-
- @classmethod
- def _get_pickling_errors(cls, obj, seen=None):
- """ Return any errors that would be raised if `obj' is pickled
- Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
- """
- if seen == None:
- seen = []
- try:
- state = obj.__getstate__()
- except AttributeError:
- return
- if state == None:
- return
- if isinstance(state, tuple):
- if not isinstance(state[0], dict):
- state = state[1]
- else:
- state = state[0].update(state[1])
- result = {}
- for i in state:
- try:
- pickle.dumps(state[i], protocol=2)
- except pickle.PicklingError:
- if not state[i] in seen:
- seen.append(state[i])
- result[i] = cls._get_pickling_errors(state[i], seen)
- return result
-
- def http_resp(self, http_method, url):
- """
- Simple wrapper for Client http requests
- Removes the `client' and `request' attributes from as they are
- added by django.test.client.Client and not part of caching
- responses outside of tests.
- """
- method = getattr(self.client, http_method)
- resp = method(url)
- del resp.client, resp.request
- return resp
-
- def test_obj_pickling(self):
- """
- Test that responses are properly pickled
- """
- resp = self.http_resp('get', '/cache')
-
- # Make sure that no pickling errors occurred
- self.assertEqual(self._get_pickling_errors(resp), {})
-
- # Unfortunately LocMem backend doesn't raise PickleErrors but returns
- # None instead.
- cache.set(self.cache_key, resp)
- self.assertTrue(cache.get(self.cache_key) is not None)
+ urls = 'tests.test_renderers'
def test_head_caching(self):
"""
Test caching of HEAD requests
"""
- resp = self.http_resp('head', '/cache')
- cache.set(self.cache_key, resp)
-
- cached_resp = cache.get(self.cache_key)
- self.assertIsInstance(cached_resp, Response)
+ response = self.client.head('/cache')
+ cache.set('key', response)
+ cached_response = cache.get('key')
+ assert isinstance(cached_response, Response)
+ assert cached_response.content == response.content
+ assert cached_response.status_code == response.status_code
def test_get_caching(self):
"""
Test caching of GET requests
"""
- resp = self.http_resp('get', '/cache')
- cache.set(self.cache_key, resp)
+ response = self.client.get('/cache')
+ cache.set('key', response)
+ cached_response = cache.get('key')
+ assert isinstance(cached_response, Response)
+ assert cached_response.content == response.content
+ assert cached_response.status_code == response.status_code
- cached_resp = cache.get(self.cache_key)
- self.assertIsInstance(cached_resp, Response)
- self.assertEqual(cached_resp.content, resp.content)
+
+class TestJSONIndentationStyles:
+ def test_indented(self):
+ renderer = JSONRenderer()
+ data = OrderedDict([('a', 1), ('b', 2)])
+ assert renderer.render(data) == b'{"a":1,"b":2}'
+
+ def test_compact(self):
+ renderer = JSONRenderer()
+ data = OrderedDict([('a', 1), ('b', 2)])
+ context = {'indent': 4}
+ assert (
+ renderer.render(data, renderer_context=context) ==
+ b'{\n "a": 1,\n "b": 2\n}'
+ )
+
+ def test_long_form(self):
+ renderer = JSONRenderer()
+ renderer.compact = False
+ data = OrderedDict([('a', 1), ('b', 2)])
+ assert renderer.render(data) == b'{"a": 1, "b": 2}'
+
+
+class TestHiddenFieldHTMLFormRenderer(TestCase):
+ def test_hidden_field_rendering(self):
+ class TestSerializer(serializers.Serializer):
+ published = serializers.HiddenField(default=True)
+
+ serializer = TestSerializer(data={})
+ serializer.is_valid()
+ renderer = HTMLFormRenderer()
+ field = serializer['published']
+ rendered = renderer.render_field(field, {})
+ assert rendered == ''
diff --git a/rest_framework/tests/test_request.py b/tests/test_request.py
similarity index 63%
rename from rest_framework/tests/test_request.py
rename to tests/test_request.py
index 969d8024a..c274ab69d 100644
--- a/rest_framework/tests/test_request.py
+++ b/tests/test_request.py
@@ -2,25 +2,27 @@
Tests for content parsing, and form-overloaded content parsing.
"""
from __future__ import unicode_literals
+from django.conf.urls import patterns
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware
+from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase
+from django.utils import six
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
-from rest_framework.compat import patterns
from rest_framework.parsers import (
BaseParser,
FormParser,
MultiPartParser,
JSONParser
)
-from rest_framework.request import Request
+from rest_framework.request import Request, Empty
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
-from rest_framework.compat import six
+from io import BytesIO
import json
@@ -66,6 +68,9 @@ class TestMethodOverloading(TestCase):
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
+ request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
+ self.assertEqual(request.method, 'DELETE')
+
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
@@ -146,88 +151,33 @@ class TestContentParsing(TestCase):
request.parsers = (JSONParser(), )
self.assertEqual(request.DATA, json_data)
- # def test_accessing_post_after_data_form(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in
- # form request.
- # """
- # data = {'qwerty': 'uiop'}
- # request = factory.post('/', data=data)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), data.items())
-
- # def test_accessing_post_after_data_for_json(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
-
- # request = factory.post('/', content, content_type=content_type,
- # parsers=parsers)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), [])
-
- # def test_accessing_post_after_data_for_overloaded_json(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in overloaded
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # form_data = {Request._CONTENT_PARAM: content,
- # Request._CONTENTTYPE_PARAM: content_type}
-
- # request = factory.post('/', form_data, parsers=parsers)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), form_data.items())
-
- # def test_accessing_data_after_post_form(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in
- # form request.
- # """
- # data = {'qwerty': 'uiop'}
- # parsers = (FormParser, MultiPartParser)
- # request = factory.post('/', data, parsers=parsers)
-
- # self.assertEqual(request.POST.items(), data.items())
- # self.assertEqual(request.DATA.items(), data.items())
-
- # def test_accessing_data_after_post_for_json(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # request = factory.post('/', content, content_type=content_type,
- # parsers=parsers)
- # self.assertEqual(request.POST.items(), [])
- # self.assertEqual(request.DATA.items(), data.items())
-
- # def test_accessing_data_after_post_for_overloaded_json(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in overloaded
- # json request
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # form_data = {Request._CONTENT_PARAM: content,
- # Request._CONTENTTYPE_PARAM: content_type}
-
- # request = factory.post('/', form_data, parsers=parsers)
- # self.assertEqual(request.POST.items(), form_data.items())
- # self.assertEqual(request.DATA.items(), data.items())
+ def test_form_POST_unicode(self):
+ """
+ JSON POST via default web interface with unicode data
+ """
+ # Note: environ and other variables here have simplified content compared to real Request
+ CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
+ environ = {
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+ 'CONTENT_LENGTH': len(CONTENT),
+ 'wsgi.input': BytesIO(CONTENT),
+ }
+ wsgi_request = WSGIRequest(environ=environ)
+ wsgi_request._load_post_and_files()
+ parsers = (JSONParser(), FormParser(), MultiPartParser())
+ parser_context = {
+ 'encoding': 'utf-8',
+ 'kwargs': {},
+ 'args': (),
+ }
+ request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
+ method = request.method
+ self.assertEqual(method, 'POST')
+ self.assertEqual(request._content_type, 'application/json')
+ self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
+ self.assertEqual(request._data, Empty)
+ self.assertEqual(request._files, Empty)
class MockView(APIView):
@@ -237,15 +187,16 @@ class MockView(APIView):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
- return Response(status=status.INTERNAL_SERVER_ERROR)
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
(r'^$', MockView.as_view()),
)
class TestContentParsingWithAuthentication(TestCase):
- urls = 'rest_framework.tests.test_request'
+ urls = 'tests.test_request'
def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True)
@@ -267,25 +218,14 @@ class TestContentParsingWithAuthentication(TestCase):
response = self.csrf_client.post('/', content)
self.assertEqual(status.HTTP_200_OK, response.status_code)
- # def test_user_logged_in_authentication_has_post_when_logged_in(self):
- # """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
- # self.client.login(username='john', password='password')
- # self.csrf_client.login(username='john', password='password')
- # content = {'example': 'example'}
-
- # response = self.client.post('/', content)
- # self.assertEqual(status.OK, response.status_code, "POST data is malformed")
-
- # response = self.csrf_client.post('/', content)
- # self.assertEqual(status.OK, response.status_code, "POST data is malformed")
-
class TestUserSetter(TestCase):
def setUp(self):
# Pass request object through session middleware so session is
# available to login and logout functions
- self.request = Request(factory.get('/'))
+ self.wrapped_request = factory.get('/')
+ self.request = Request(self.wrapped_request)
SessionMiddleware().process_request(self.request)
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
@@ -305,9 +245,33 @@ class TestUserSetter(TestCase):
logout(self.request)
self.assertTrue(self.request.user.is_anonymous())
+ def test_logged_in_user_is_set_on_wrapped_request(self):
+ login(self.request, self.user)
+ self.assertEqual(self.wrapped_request.user, self.user)
+
+ def test_calling_user_fails_when_attribute_error_is_raised(self):
+ """
+ This proves that when an AttributeError is raised inside of the request.user
+ property, that we can handle this and report the true, underlying error.
+ """
+ class AuthRaisesAttributeError(object):
+ def authenticate(self, request):
+ import rest_framework
+ rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
+
+ self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
+ SessionMiddleware().process_request(self.request)
+
+ login(self.request, self.user)
+ try:
+ self.request.user
+ except AttributeError as error:
+ self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'")
+ else:
+ assert False, 'AttributeError not raised'
+
class TestAuthSetter(TestCase):
-
def test_auth_can_be_set(self):
request = Request(factory.get('/'))
request.auth = 'DUMMY'
diff --git a/rest_framework/tests/test_response.py b/tests/test_response.py
similarity index 88%
rename from rest_framework/tests/test_response.py
rename to tests/test_response.py
index eea3c6418..4a9deaa29 100644
--- a/rest_framework/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,11 +1,13 @@
from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
from django.test import TestCase
-from rest_framework.tests.models import BasicModel, BasicModelSerializer
-from rest_framework.compat import patterns, url, include
+from django.utils import six
+from tests.models import BasicModel
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import generics
from rest_framework import routers
+from rest_framework import serializers
from rest_framework import status
from rest_framework.renderers import (
BaseRenderer,
@@ -14,7 +16,12 @@ from rest_framework.renderers import (
)
from rest_framework import viewsets
from rest_framework.settings import api_settings
-from rest_framework.compat import six
+
+
+# Serializer used to test BasicModel
+class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
class MockPickleRenderer(BaseRenderer):
@@ -31,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+ return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+ return ('Renderer B: %s' % x).encode('ascii')
class RendererA(BaseRenderer):
@@ -86,21 +98,23 @@ class HTMLView1(APIView):
class HTMLNewModelViewSet(viewsets.ModelViewSet):
- model = BasicModel
+ serializer_class = BasicModelSerializer
+ queryset = BasicModel.objects.all()
class HTMLNewModelView(generics.ListCreateAPIView):
renderer_classes = (BrowsableAPIRenderer,)
permission_classes = []
serializer_class = BasicModelSerializer
- model = BasicModel
+ queryset = BasicModel.objects.all()
new_model_viewset_router = routers.DefaultRouter()
new_model_viewset_router.register(r'', HTMLNewModelViewSet)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^.*\.(?P.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
@@ -118,7 +132,7 @@ class RendererIntegrationTests(TestCase):
End-to-end testing of renderers using an ResponseMixin on a generic view.
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
@@ -198,7 +212,7 @@ class Issue122Tests(TestCase):
"""
Tests that covers #122.
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_only_html_renderer(self):
"""
@@ -218,13 +232,13 @@ class Issue467Tests(TestCase):
Tests for #467
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
class Issue807Tests(TestCase):
@@ -232,7 +246,7 @@ class Issue807Tests(TestCase):
Covers #807
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_does_not_append_charset_by_default(self):
"""
@@ -253,9 +267,9 @@ class Issue807Tests(TestCase):
expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
self.assertEqual(expected, resp['Content-Type'])
- def test_content_type_set_explictly_on_response(self):
+ def test_content_type_set_explicitly_on_response(self):
"""
- The content type may be set explictly on the response.
+ The content type may be set explicitly on the response.
"""
headers = {"HTTP_ACCEPT": RendererC.media_type}
resp = self.client.get('/setbyview', **headers)
@@ -268,11 +282,11 @@ class Issue807Tests(TestCase):
)
resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
diff --git a/rest_framework/tests/test_reverse.py b/tests/test_reverse.py
similarity index 82%
rename from rest_framework/tests/test_reverse.py
rename to tests/test_reverse.py
index 690a30b11..675a9d5a0 100644
--- a/rest_framework/tests/test_reverse.py
+++ b/tests/test_reverse.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
+from django.conf.urls import patterns, url
from django.test import TestCase
-from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory
@@ -10,7 +10,8 @@ factory = APIRequestFactory()
def null_view(request):
pass
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^view$', null_view, name='view'),
)
@@ -19,7 +20,7 @@ class ReverseTests(TestCase):
"""
Tests for fully qualified URLs when using `reverse`.
"""
- urls = 'rest_framework.tests.test_reverse'
+ urls = 'tests.test_reverse'
def test_reversed_urls_are_fully_qualified(self):
request = factory.get('/view')
diff --git a/tests/test_routers.py b/tests/test_routers.py
new file mode 100644
index 000000000..08c58ec70
--- /dev/null
+++ b/tests/test_routers.py
@@ -0,0 +1,348 @@
+from __future__ import unicode_literals
+from django.conf.urls import url, include
+from django.db import models
+from django.test import TestCase
+from django.core.exceptions import ImproperlyConfigured
+from rest_framework import serializers, viewsets, permissions
+from rest_framework.decorators import detail_route, list_route
+from rest_framework.response import Response
+from rest_framework.routers import SimpleRouter, DefaultRouter
+from rest_framework.test import APIRequestFactory
+from collections import namedtuple
+
+factory = APIRequestFactory()
+
+
+class RouterTestModel(models.Model):
+ uuid = models.CharField(max_length=20)
+ text = models.CharField(max_length=200)
+
+
+class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
+
+ class Meta:
+ model = RouterTestModel
+ fields = ('url', 'uuid', 'text')
+
+
+class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ serializer_class = NoteSerializer
+ lookup_field = 'uuid'
+
+
+class MockViewSet(viewsets.ModelViewSet):
+ queryset = None
+ serializer_class = None
+
+
+notes_router = SimpleRouter()
+notes_router.register(r'notes', NoteViewSet)
+
+namespaced_router = DefaultRouter()
+namespaced_router.register(r'example', MockViewSet, base_name='example')
+
+urlpatterns = [
+ url(r'^non-namespaced/', include(namespaced_router.urls)),
+ url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
+ url(r'^example/', include(notes_router.urls)),
+]
+
+
+class BasicViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'method': 'list'})
+
+ @detail_route(methods=['post'])
+ def action1(self, request, *args, **kwargs):
+ return Response({'method': 'action1'})
+
+ @detail_route(methods=['post'])
+ def action2(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @detail_route(methods=['post', 'delete'])
+ def action3(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @detail_route()
+ def link1(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route()
+ def link2(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+
+class TestSimpleRouter(TestCase):
+ def setUp(self):
+ self.router = SimpleRouter()
+
+ def test_link_and_action_decorator(self):
+ routes = self.router.get_routes(BasicViewSet)
+ decorator_routes = routes[2:]
+ # Make sure all these endpoints exist and none have been clobbered
+ for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
+ route = decorator_routes[i]
+ # check url listing
+ self.assertEqual(route.url,
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ # check method to function mapping
+ if endpoint == 'action3':
+ methods_map = ['post', 'delete']
+ elif endpoint.startswith('action'):
+ methods_map = ['post']
+ else:
+ methods_map = ['get']
+ for method in methods_map:
+ self.assertEqual(route.mapping[method], endpoint)
+
+
+class TestRootView(TestCase):
+ urls = 'tests.test_routers'
+
+ def test_retrieve_namespaced_root(self):
+ response = self.client.get('/namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/namespaced/example/",
+ }
+ )
+
+ def test_retrieve_non_namespaced_root(self):
+ response = self.client.get('/non-namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/non-namespaced/example/",
+ }
+ )
+
+
+class TestCustomLookupFields(TestCase):
+ """
+ Ensure that custom lookup fields are correctly routed.
+ """
+ urls = 'tests.test_routers'
+
+ def setUp(self):
+ RouterTestModel.objects.create(uuid='123', text='foo bar')
+
+ def test_custom_lookup_field_route(self):
+ detail_route = notes_router.urls[-1]
+ detail_url_pattern = detail_route.regex.pattern
+ self.assertIn('', detail_url_pattern)
+
+ def test_retrieve_lookup_field_list_view(self):
+ response = self.client.get('/example/notes/')
+ self.assertEqual(
+ response.data,
+ [{
+ "url": "http://testserver/example/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }]
+ )
+
+ def test_retrieve_lookup_field_detail_view(self):
+ response = self.client.get('/example/notes/123/')
+ self.assertEqual(
+ response.data,
+ {
+ "url": "http://testserver/example/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }
+ )
+
+
+class TestLookupValueRegex(TestCase):
+ """
+ Ensure the router honors lookup_value_regex when applied
+ to the viewset.
+ """
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ lookup_field = 'uuid'
+ lookup_value_regex = '[0-9a-f]{32}'
+
+ self.router = SimpleRouter()
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_limited_by_lookup_value_regex(self):
+ expected = ['^notes/$', '^notes/(?P[0-9a-f]{32})/$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestTrailingSlashIncluded(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = SimpleRouter()
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_have_trailing_slash_by_default(self):
+ expected = ['^notes/$', '^notes/(?P[^/.]+)/$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestTrailingSlashRemoved(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = SimpleRouter(trailing_slash=False)
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_can_have_trailing_slash_removed(self):
+ expected = ['^notes$', '^notes/(?P[^/.]+)$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestNameableRoot(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = DefaultRouter()
+ self.router.root_view_name = 'nameable-root'
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_router_has_custom_name(self):
+ expected = 'nameable-root'
+ self.assertEqual(expected, self.urls[0].name)
+
+
+class TestActionKeywordArgs(TestCase):
+ """
+ Ensure keyword arguments passed in the `@action` decorator
+ are properly handled. Refs #940.
+ """
+
+ def setUp(self):
+ class TestViewSet(viewsets.ModelViewSet):
+ permission_classes = []
+
+ @detail_route(methods=['post'], permission_classes=[permissions.AllowAny])
+ def custom(self, request, *args, **kwargs):
+ return Response({
+ 'permission_classes': self.permission_classes
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+ self.view = self.router.urls[-1].callback
+
+ def test_action_kwargs(self):
+ request = factory.post('/test/0/custom/')
+ response = self.view(request)
+ self.assertEqual(
+ response.data,
+ {'permission_classes': [permissions.AllowAny]}
+ )
+
+
+class TestActionAppliedToExistingRoute(TestCase):
+ """
+ Ensure `@detail_route` decorator raises an except when applied
+ to an existing route
+ """
+
+ def test_exception_raised_when_action_applied_to_existing_route(self):
+ class TestViewSet(viewsets.ModelViewSet):
+
+ @detail_route(methods=['post'])
+ def retrieve(self, request, *args, **kwargs):
+ return Response({
+ 'hello': 'world'
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+
+ with self.assertRaises(ImproperlyConfigured):
+ self.router.urls
+
+
+class DynamicListAndDetailViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'method': 'list'})
+
+ @list_route(methods=['post'])
+ def list_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action1'})
+
+ @detail_route(methods=['post'])
+ def detail_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @list_route()
+ def list_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route()
+ def detail_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+ @list_route(url_path="list_custom-route")
+ def list_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route(url_path="detail_custom-route")
+ def detail_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+
+class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet):
+ pass
+
+
+class TestDynamicListAndDetailRouter(TestCase):
+ def setUp(self):
+ self.router = SimpleRouter()
+
+ def _test_list_and_detail_route_decorators(self, viewset):
+ routes = self.router.get_routes(viewset)
+ decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
+
+ MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
+ # Make sure all these endpoints exist and none have been clobbered
+ for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
+ MethodNamesMap('list_route_get', 'list_route_get'),
+ MethodNamesMap('list_route_post', 'list_route_post'),
+ MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
+ MethodNamesMap('detail_route_get', 'detail_route_get'),
+ MethodNamesMap('detail_route_post', 'detail_route_post')
+ ]):
+ route = decorator_routes[i]
+ # check url listing
+ method_name = endpoint.method_name
+ url_path = endpoint.url_path
+
+ if method_name.startswith('list_'):
+ self.assertEqual(route.url,
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
+ else:
+ self.assertEqual(route.url,
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
+ # check method to function mapping
+ if method_name.endswith('_post'):
+ method_map = 'post'
+ else:
+ method_map = 'get'
+ self.assertEqual(route.mapping[method_map], method_name)
+
+ def test_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
+
+ def test_inherited_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
new file mode 100644
index 000000000..b7a0484bc
--- /dev/null
+++ b/tests/test_serializer.py
@@ -0,0 +1,297 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from .utils import MockObject
+from rest_framework import serializers
+from rest_framework.compat import unicode_repr
+import pickle
+import pytest
+
+
+# Tests for core functionality.
+# -----------------------------
+
+class TestSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+ self.Serializer = ExampleSerializer
+
+ def test_valid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc', 'integer': 123}
+ assert serializer.errors == {}
+
+ def test_invalid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc'})
+ assert not serializer.is_valid()
+ assert serializer.validated_data == {}
+ assert serializer.errors == {'integer': ['This field is required.']}
+
+ def test_partial_validation(self):
+ serializer = self.Serializer(data={'char': 'abc'}, partial=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc'}
+ assert serializer.errors == {}
+
+ def test_empty_serializer(self):
+ serializer = self.Serializer()
+ assert serializer.data == {'char': '', 'integer': None}
+
+ def test_missing_attribute_during_serialization(self):
+ class MissingAttributes:
+ pass
+ instance = MissingAttributes()
+ serializer = self.Serializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
+
+
+class TestValidateMethod:
+ def test_non_field_error_validate_method(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+
+ def validate(self, attrs):
+ raise serializers.ValidationError('Non field error')
+
+ serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123})
+ assert not serializer.is_valid()
+ assert serializer.errors == {'non_field_errors': ['Non field error']}
+
+ def test_field_error_validate_method(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+
+ def validate(self, attrs):
+ raise serializers.ValidationError({'char': 'Field error'})
+
+ serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123})
+ assert not serializer.is_valid()
+ assert serializer.errors == {'char': ['Field error']}
+
+
+class TestBaseSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'id': obj['id'],
+ 'email': obj['name'] + '@' + obj['domain']
+ }
+
+ def to_internal_value(self, data):
+ name, domain = str(data['email']).split('@')
+ return {
+ 'id': int(data['id']),
+ 'name': name,
+ 'domain': domain,
+ }
+
+ self.Serializer = ExampleSerializer
+
+ def test_serialize_instance(self):
+ instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'id': 1, 'email': 'tom@example.com'}
+
+ def test_serialize_list(self):
+ instances = [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'},
+ ]
+ serializer = self.Serializer(instances, many=True)
+ assert serializer.data == [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'}
+ ]
+
+ def test_validate_data(self):
+ data = {'id': 1, 'email': 'tom@example.com'}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'id': 1,
+ 'name': 'tom',
+ 'domain': 'example.com'
+ }
+
+ def test_validate_list(self):
+ data = [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'},
+ ]
+ serializer = self.Serializer(data=data, many=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'}
+ ]
+
+
+class TestStarredSource:
+ """
+ Tests for `source='*'` argument, which is used for nested representations.
+
+ For example:
+
+ nested_field = NestedField(source='*')
+ """
+ data = {
+ 'nested1': {'a': 1, 'b': 2},
+ 'nested2': {'c': 3, 'd': 4}
+ }
+
+ def setup(self):
+ class NestedSerializer1(serializers.Serializer):
+ a = serializers.IntegerField()
+ b = serializers.IntegerField()
+
+ class NestedSerializer2(serializers.Serializer):
+ c = serializers.IntegerField()
+ d = serializers.IntegerField()
+
+ class TestSerializer(serializers.Serializer):
+ nested1 = NestedSerializer1(source='*')
+ nested2 = NestedSerializer2(source='*')
+
+ self.Serializer = TestSerializer
+
+ def test_nested_validate(self):
+ """
+ A nested representation is validated into a flat internal object.
+ """
+ serializer = self.Serializer(data=self.data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ 'd': 4
+ }
+
+ def test_nested_serialize(self):
+ """
+ An object can be serialized into a nested representation.
+ """
+ instance = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
+ serializer = self.Serializer(instance)
+ assert serializer.data == self.data
+
+
+class TestIncorrectlyConfigured:
+ def test_incorrect_field_name(self):
+ class ExampleSerializer(serializers.Serializer):
+ incorrect_name = serializers.IntegerField()
+
+ class ExampleObject:
+ def __init__(self):
+ self.correct_name = 123
+
+ instance = ExampleObject()
+ serializer = ExampleSerializer(instance)
+ with pytest.raises(AttributeError) as exc_info:
+ serializer.data
+ msg = str(exc_info.value)
+ assert msg.startswith(
+ "Got AttributeError when attempting to get a value for field `incorrect_name` on serializer `ExampleSerializer`.\n"
+ "The serializer field might be named incorrectly and not match any attribute or key on the `ExampleObject` instance.\n"
+ "Original exception text was:"
+ )
+
+
+class TestUnicodeRepr:
+ def test_unicode_repr(self):
+ class ExampleSerializer(serializers.Serializer):
+ example = serializers.CharField()
+
+ class ExampleObject:
+ def __init__(self):
+ self.example = '한국'
+
+ def __repr__(self):
+ return unicode_repr(self.example)
+
+ instance = ExampleObject()
+ serializer = ExampleSerializer(instance)
+ repr(serializer) # Should not error.
+
+
+class TestNotRequiredOutput:
+ def test_not_required_output_for_dict(self):
+ """
+ 'required=False' should allow a dictionary key to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_not_required_output_for_object(self):
+ """
+ 'required=False' should allow an object attribute to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ def create(self, validated_data):
+ return MockObject(**validated_data)
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ serializer.save()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_default_required_output_for_dict(self):
+ """
+ 'default="something"' should require dictionary key.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer({'included': 'abc'})
+ with pytest.raises(KeyError):
+ serializer.data
+
+ def test_default_required_output_for_object(self):
+ """
+ 'default="something"' should require object attribute.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ instance = MockObject(included='abc')
+ serializer = ExampleSerializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
+
+
+class TestCacheSerializerData:
+ def test_cache_serializer_data(self):
+ """
+ Caching serializer data with pickle will drop the serializer info,
+ but does preserve the data itself.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ field1 = serializers.CharField()
+ field2 = serializers.CharField()
+
+ serializer = ExampleSerializer({'field1': 'a', 'field2': 'b'})
+ pickled = pickle.dumps(serializer.data)
+ data = pickle.loads(pickled)
+ assert data == {'field1': 'a', 'field2': 'b'}
diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py
new file mode 100644
index 000000000..bc955b2ef
--- /dev/null
+++ b/tests/test_serializer_bulk_update.py
@@ -0,0 +1,123 @@
+"""
+Tests to cover bulk create and update using serializers.
+"""
+from __future__ import unicode_literals
+from django.test import TestCase
+from django.utils import six
+from rest_framework import serializers
+
+
+class BulkCreateSerializerTests(TestCase):
+ """
+ Creating multiple instances using serializers.
+ """
+
+ def setUp(self):
+ class BookSerializer(serializers.Serializer):
+ id = serializers.IntegerField()
+ title = serializers.CharField(max_length=100)
+ author = serializers.CharField(max_length=100)
+
+ self.BookSerializer = BookSerializer
+
+ def test_bulk_create_success(self):
+ """
+ Correct bulk update serialization should return the input data.
+ """
+
+ data = [
+ {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }, {
+ 'id': 1,
+ 'title': 'If this is a man',
+ 'author': 'Primo Levi'
+ }, {
+ 'id': 2,
+ 'title': 'The wind-up bird chronicle',
+ 'author': 'Haruki Murakami'
+ }
+ ]
+
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.validated_data, data)
+
+ def test_bulk_create_errors(self):
+ """
+ Incorrect bulk create serialization should return errors.
+ """
+
+ data = [
+ {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }, {
+ 'id': 1,
+ 'title': 'If this is a man',
+ 'author': 'Primo Levi'
+ }, {
+ 'id': 'foo',
+ 'title': 'The wind-up bird chronicle',
+ 'author': 'Haruki Murakami'
+ }
+ ]
+ expected_errors = [
+ {},
+ {},
+ {'id': ['A valid integer is required.']}
+ ]
+
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_list_datatype(self):
+ """
+ Data containing list of incorrect data type should return errors.
+ """
+ data = ['foo', 'bar', 'baz']
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ text_type_string = six.text_type.__name__
+ message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
+ expected_errors = [
+ {'non_field_errors': [message]},
+ {'non_field_errors': [message]},
+ {'non_field_errors': [message]}
+ ]
+
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_single_datatype(self):
+ """
+ Data containing a single incorrect data type should return errors.
+ """
+ data = 123
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
+
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_single_object(self):
+ """
+ Data containing only a single object, instead of a list of objects
+ should return errors.
+ """
+ data = {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
+
+ self.assertEqual(serializer.errors, expected_errors)
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
new file mode 100644
index 000000000..35b68ae7d
--- /dev/null
+++ b/tests/test_serializer_lists.py
@@ -0,0 +1,290 @@
+from rest_framework import serializers
+from django.utils.datastructures import MultiValueDict
+
+
+class BasicObject:
+ """
+ A mock object for testing serializer save behavior.
+ """
+ def __init__(self, **kwargs):
+ self._data = kwargs
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def __eq__(self, other):
+ if self._data.keys() != other._data.keys():
+ return False
+ for key in self._data.keys():
+ if self._data[key] != other._data[key]:
+ return False
+ return True
+
+
+class TestListSerializer:
+ """
+ Tests for using a ListSerializer as a top-level serializer.
+ Note that this is in contrast to using ListSerializer as a field.
+ """
+
+ def setup(self):
+ class IntegerListSerializer(serializers.ListSerializer):
+ child = serializers.IntegerField()
+ self.Serializer = IntegerListSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of items should return a list of validated items.
+ """
+ input_data = ["123", "456"]
+ expected_output = [123, 456]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x] style ids.
+ """
+ input_data = MultiValueDict({"[0]": ["123"], "[1]": ["456"]})
+ expected_output = [123, 456]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestListSerializerContainingNestedSerializer:
+ """
+ Tests for using a ListSerializer containing another serializer.
+ """
+
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integer = serializers.IntegerField()
+ boolean = serializers.BooleanField()
+
+ def create(self, validated_data):
+ return BasicObject(**validated_data)
+
+ class ObjectListSerializer(serializers.ListSerializer):
+ child = TestSerializer()
+
+ self.Serializer = ObjectListSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of dictionaries should return a list of
+ validated dictionaries.
+ """
+ input_data = [
+ {"integer": "123", "boolean": "true"},
+ {"integer": "456", "boolean": "false"}
+ ]
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_create(self):
+ """
+ Creating from a list of dictionaries should return a list of objects.
+ """
+ input_data = [
+ {"integer": "123", "boolean": "true"},
+ {"integer": "456", "boolean": "false"}
+ ]
+ expected_output = [
+ BasicObject(integer=123, boolean=True),
+ BasicObject(integer=456, boolean=False),
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.save() == expected_output
+
+ def test_serialize(self):
+ """
+ Serialization of a list of objects should return a list of dictionaries.
+ """
+ input_objects = [
+ BasicObject(integer=123, boolean=True),
+ BasicObject(integer=456, boolean=False)
+ ]
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(input_objects)
+ assert serializer.data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "[0]integer": ["123"],
+ "[0]boolean": ["true"],
+ "[1]integer": ["456"],
+ "[1]boolean": ["false"]
+ })
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestNestedListSerializer:
+ """
+ Tests for using a ListSerializer as a field.
+ """
+
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integers = serializers.ListSerializer(child=serializers.IntegerField())
+ booleans = serializers.ListSerializer(child=serializers.BooleanField())
+
+ def create(self, validated_data):
+ return BasicObject(**validated_data)
+
+ self.Serializer = TestSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of items should return a list of validated items.
+ """
+ input_data = {
+ "integers": ["123", "456"],
+ "booleans": ["true", "false"]
+ }
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_create(self):
+ """
+ Creation with a list of items return an object with an attribute that
+ is a list of items.
+ """
+ input_data = {
+ "integers": ["123", "456"],
+ "booleans": ["true", "false"]
+ }
+ expected_output = BasicObject(
+ integers=[123, 456],
+ booleans=[True, False]
+ )
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.save() == expected_output
+
+ def test_serialize(self):
+ """
+ Serialization of a list of items should return a list of items.
+ """
+ input_object = BasicObject(
+ integers=[123, 456],
+ booleans=[True, False]
+ )
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(input_object)
+ assert serializer.data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "integers[0]": ["123"],
+ "integers[1]": ["456"],
+ "booleans[0]": ["true"],
+ "booleans[1]": ["false"]
+ })
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestNestedListOfListsSerializer:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integers = serializers.ListSerializer(
+ child=serializers.ListSerializer(
+ child=serializers.IntegerField()
+ )
+ )
+ booleans = serializers.ListSerializer(
+ child=serializers.ListSerializer(
+ child=serializers.BooleanField()
+ )
+ )
+
+ self.Serializer = TestSerializer
+
+ def test_validate(self):
+ input_data = {
+ 'integers': [['123', '456'], ['789', '0']],
+ 'booleans': [['true', 'true'], ['false', 'true']]
+ }
+ expected_output = {
+ "integers": [[123, 456], [789, 0]],
+ "booleans": [[True, True], [False, True]]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock lists of lists using [x][y]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "integers[0][0]": ["123"],
+ "integers[0][1]": ["456"],
+ "integers[1][0]": ["789"],
+ "integers[1][1]": ["000"],
+ "booleans[0][0]": ["true"],
+ "booleans[0][1]": ["true"],
+ "booleans[1][0]": ["false"],
+ "booleans[1][1]": ["true"]
+ })
+ expected_output = {
+ "integers": [[123, 456], [789, 0]],
+ "booleans": [[True, True], [False, True]]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestListSerializerClass:
+ """Tests for a custom list_serializer_class."""
+ def test_list_serializer_class_validate(self):
+ class CustomListSerializer(serializers.ListSerializer):
+ def validate(self, attrs):
+ raise serializers.ValidationError('Non field error')
+
+ class TestSerializer(serializers.Serializer):
+ class Meta:
+ list_serializer_class = CustomListSerializer
+
+ serializer = TestSerializer(data=[], many=True)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'non_field_errors': ['Non field error']}
diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py
new file mode 100644
index 000000000..f5e4b26ad
--- /dev/null
+++ b/tests/test_serializer_nested.py
@@ -0,0 +1,40 @@
+from rest_framework import serializers
+
+
+class TestNestedSerializer:
+ def setup(self):
+ class NestedSerializer(serializers.Serializer):
+ one = serializers.IntegerField(max_value=10)
+ two = serializers.IntegerField(max_value=10)
+
+ class TestSerializer(serializers.Serializer):
+ nested = NestedSerializer()
+
+ self.Serializer = TestSerializer
+
+ def test_nested_validate(self):
+ input_data = {
+ 'nested': {
+ 'one': '1',
+ 'two': '2',
+ }
+ }
+ expected_data = {
+ 'nested': {
+ 'one': 1,
+ 'two': 2,
+ }
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_data
+
+ def test_nested_serialize_empty(self):
+ expected_data = {
+ 'nested': {
+ 'one': None,
+ 'two': None
+ }
+ }
+ serializer = self.Serializer()
+ assert serializer.data == expected_data
diff --git a/tests/test_settings.py b/tests/test_settings.py
new file mode 100644
index 000000000..f2ff4ca14
--- /dev/null
+++ b/tests/test_settings.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.settings import APISettings
+
+
+class TestSettings(TestCase):
+ def test_import_error_message_maintained(self):
+ """
+ Make sure import errors are captured and raised sensibly.
+ """
+ settings = APISettings({
+ 'DEFAULT_RENDERER_CLASSES': [
+ 'tests.invalid_module.InvalidClassName'
+ ]
+ })
+ with self.assertRaises(ImportError):
+ settings.DEFAULT_RENDERER_CLASSES
diff --git a/tests/test_status.py b/tests/test_status.py
new file mode 100644
index 000000000..721a6e30b
--- /dev/null
+++ b/tests/test_status.py
@@ -0,0 +1,33 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.status import (
+ is_informational, is_success, is_redirect, is_client_error, is_server_error
+)
+
+
+class TestStatus(TestCase):
+ def test_status_categories(self):
+ self.assertFalse(is_informational(99))
+ self.assertTrue(is_informational(100))
+ self.assertTrue(is_informational(199))
+ self.assertFalse(is_informational(200))
+
+ self.assertFalse(is_success(199))
+ self.assertTrue(is_success(200))
+ self.assertTrue(is_success(299))
+ self.assertFalse(is_success(300))
+
+ self.assertFalse(is_redirect(299))
+ self.assertTrue(is_redirect(300))
+ self.assertTrue(is_redirect(399))
+ self.assertFalse(is_redirect(400))
+
+ self.assertFalse(is_client_error(399))
+ self.assertTrue(is_client_error(400))
+ self.assertTrue(is_client_error(499))
+ self.assertFalse(is_client_error(500))
+
+ self.assertFalse(is_server_error(499))
+ self.assertTrue(is_server_error(500))
+ self.assertTrue(is_server_error(599))
+ self.assertFalse(is_server_error(600))
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
new file mode 100644
index 000000000..0cee91f19
--- /dev/null
+++ b/tests/test_templatetags.py
@@ -0,0 +1,75 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.test import APIRequestFactory
+from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links
+
+
+factory = APIRequestFactory()
+
+
+class TemplateTagTests(TestCase):
+
+ def test_add_query_param_with_non_latin_charactor(self):
+ # Ensure we don't double-escape non-latin characters
+ # that are present in the querystring.
+ # See #1314.
+ request = factory.get("/", {'q': '查询'})
+ json_url = add_query_param(request, "format", "json")
+ self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
+ self.assertIn("format=json", json_url)
+
+
+class Issue1386Tests(TestCase):
+ """
+ Covers #1386
+ """
+
+ def test_issue_1386(self):
+ """
+ Test function urlize_quoted_links with different args
+ """
+ correct_urls = [
+ "asdf.com",
+ "asdf.net",
+ "www.as_df.org",
+ "as.d8f.ghj8.gov",
+ ]
+ for i in correct_urls:
+ res = urlize_quoted_links(i)
+ self.assertNotEqual(res, i)
+ self.assertIn(i, res)
+
+ incorrect_urls = [
+ "mailto://asdf@fdf.com",
+ "asdf.netnet",
+ ]
+ for i in incorrect_urls:
+ res = urlize_quoted_links(i)
+ self.assertEqual(i, res)
+
+ # example from issue #1386, this shouldn't raise an exception
+ urlize_quoted_links("asdf:[/p]zxcv.com")
+
+
+class URLizerTests(TestCase):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ def _urlize_dict_check(self, data):
+ """
+ For all items in dict test assert that the value is urlized key
+ """
+ for original, urlized in data.items():
+ assert urlize_quoted_links(original, nofollow=False) == urlized
+
+ def test_json_with_url(self):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ data = {}
+ data['"url": "http://api/users/1/", '] = \
+ '"url": "http://api/users/1/", '
+ data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
+ '"foo_set": [\n "http://api/foos/1/"\n], '
+ self._urlize_dict_check(data)
diff --git a/tests/test_testing.py b/tests/test_testing.py
new file mode 100644
index 000000000..87d2b61fa
--- /dev/null
+++ b/tests/test_testing.py
@@ -0,0 +1,234 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url
+from django.contrib.auth.models import User
+from django.shortcuts import redirect
+from django.test import TestCase
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
+from io import BytesIO
+
+
+@api_view(['GET', 'POST'])
+def view(request):
+ return Response({
+ 'auth': request.META.get('HTTP_AUTHORIZATION', b''),
+ 'user': request.user.username
+ })
+
+
+@api_view(['GET', 'POST'])
+def session_view(request):
+ active_session = request.session.get('active_session', False)
+ request.session['active_session'] = True
+ return Response({
+ 'active_session': active_session
+ })
+
+
+@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
+def redirect_view(request):
+ return redirect('/view/')
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^view/$', view),
+ url(r'^session-view/$', session_view),
+ url(r'^redirect-view/$', redirect_view),
+)
+
+
+class TestAPITestClient(TestCase):
+ urls = 'tests.test_testing'
+
+ def setUp(self):
+ self.client = APIClient()
+
+ def test_credentials(self):
+ """
+ Setting `.credentials()` adds the required headers to each request.
+ """
+ self.client.credentials(HTTP_AUTHORIZATION='example')
+ for _ in range(0, 3):
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], 'example')
+
+ def test_force_authenticate(self):
+ """
+ Setting `.force_authenticate()` forcibly authenticates each request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ self.client.force_authenticate(user)
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], 'example')
+
+ def test_force_authenticate_with_sessions(self):
+ """
+ Setting `.force_authenticate()` forcibly authenticates each request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ self.client.force_authenticate(user)
+
+ # First request does not yet have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
+ # Subsequant requests have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], True)
+
+ # Force authenticating as `None` should also logout the user session.
+ self.client.force_authenticate(None)
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
+ def test_csrf_exempt_by_default(self):
+ """
+ By default, the test client is CSRF exempt.
+ """
+ User.objects.create_user('example', 'example@example.com', 'password')
+ self.client.login(username='example', password='password')
+ response = self.client.post('/view/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_explicitly_enforce_csrf_checks(self):
+ """
+ The test client can enforce CSRF checks.
+ """
+ client = APIClient(enforce_csrf_checks=True)
+ User.objects.create_user('example', 'example@example.com', 'password')
+ client.login(username='example', password='password')
+ response = client.post('/view/')
+ expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(response.data, expected)
+
+ def test_can_logout(self):
+ """
+ `logout()` resets stored credentials
+ """
+ self.client.credentials(HTTP_AUTHORIZATION='example')
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], 'example')
+ self.client.logout()
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], b'')
+
+ def test_logout_resets_force_authenticate(self):
+ """
+ `logout()` resets any `force_authenticate`
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ self.client.force_authenticate(user)
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], 'example')
+ self.client.logout()
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], '')
+
+ def test_follow_redirect(self):
+ """
+ Follow redirect by setting follow argument.
+ """
+ response = self.client.get('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.get('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.post('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.put('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.put('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.patch('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.patch('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.delete('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.delete('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.options('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.options('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+
+class TestAPIRequestFactory(TestCase):
+ def test_csrf_exempt_by_default(self):
+ """
+ By default, the test client is CSRF exempt.
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ factory = APIRequestFactory()
+ request = factory.post('/view/')
+ request.user = user
+ response = view(request)
+ self.assertEqual(response.status_code, 200)
+
+ def test_explicitly_enforce_csrf_checks(self):
+ """
+ The test client can enforce CSRF checks.
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ factory = APIRequestFactory(enforce_csrf_checks=True)
+ request = factory.post('/view/')
+ request.user = user
+ response = view(request)
+ expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(response.data, expected)
+
+ def test_invalid_format(self):
+ """
+ Attempting to use a format that is not configured will raise an
+ assertion error.
+ """
+ factory = APIRequestFactory()
+ self.assertRaises(
+ AssertionError, factory.post,
+ path='/view/', data={'example': 1}, format='xml'
+ )
+
+ def test_force_authenticate(self):
+ """
+ Setting `force_authenticate()` forcibly authenticates the request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ factory = APIRequestFactory()
+ request = factory.get('/view')
+ force_authenticate(request, user=user)
+ response = view(request)
+ self.assertEqual(response.data['user'], 'example')
+
+ def test_upload_file(self):
+ # This is a 1x1 black png
+ simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82')
+ simple_png.name = 'test.png'
+ factory = APIRequestFactory()
+ factory.post('/', data={'image': simple_png})
+
+ def test_request_factory_url_arguments(self):
+ """
+ This is a non regression test against #1461
+ """
+ factory = APIRequestFactory()
+ request = factory.get('/view/?demo=test')
+ self.assertEqual(dict(request.GET), {'demo': ['test']})
+ request = factory.get('/view/', {'demo': 'test'})
+ self.assertEqual(dict(request.GET), {'demo': ['test']})
diff --git a/rest_framework/tests/test_throttling.py b/tests/test_throttling.py
similarity index 61%
rename from rest_framework/tests/test_throttling.py
rename to tests/test_throttling.py
index 19bc691ae..50a53b3eb 100644
--- a/rest_framework/tests/test_throttling.py
+++ b/tests/test_throttling.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.cache import cache
+from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
-from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle
+from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle
from rest_framework.response import Response
@@ -21,6 +22,14 @@ class User3MinRateThrottle(UserRateThrottle):
scope = 'minutes'
+class NonTimeThrottle(BaseThrottle):
+ def allow_request(self, request, view):
+ if not hasattr(self.__class__, 'called'):
+ self.__class__.called = True
+ return True
+ return False
+
+
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
@@ -35,6 +44,13 @@ class MockView_MinuteThrottling(APIView):
return Response('foo')
+class MockView_NonTimeThrottling(APIView):
+ throttle_classes = (NonTimeThrottle,)
+
+ def get(self, request):
+ return Response('foo')
+
+
class ThrottlingTests(TestCase):
def setUp(self):
"""
@@ -93,7 +109,7 @@ class ThrottlingTests(TestCase):
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
- Ensure the response returns an X-Throttle field with status and next attributes
+ Ensure the response returns an Retry-After field with status and next attributes
set properly.
"""
request = self.factory.get('/')
@@ -101,44 +117,66 @@ class ThrottlingTests(TestCase):
self.set_throttle_timer(view, timer)
response = view.as_view()(request)
if expect is not None:
- self.assertEqual(response['X-Throttle-Wait-Seconds'], expect)
+ self.assertEqual(response['Retry-After'], expect)
else:
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
+ self.assertFalse('Retry-After' in response)
def test_seconds_fields(self):
"""
Ensure for second based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView,
- ((0, None),
- (0, None),
- (0, None),
- (0, '1')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '1')
+ )
+ )
def test_minutes_fields(self):
"""
Ensure for minute based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (0, None),
- (0, None),
- (0, '60')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '60')
+ )
+ )
def test_next_rate_remains_constant_if_followed(self):
"""
If a client follows the recommended next request rate,
the throttling rate should stay constant.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (20, None),
- (40, None),
- (60, None),
- (80, None)
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (20, None),
+ (40, None),
+ (60, None),
+ (80, None)
+ )
+ )
+
+ def test_non_time_throttle(self):
+ """
+ Ensure for second based throttles.
+ """
+ request = self.factory.get('/')
+
+ self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('Retry-After' in response)
+
+ self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('Retry-After' in response)
class ScopedRateThrottleTests(TestCase):
@@ -150,7 +188,9 @@ class ScopedRateThrottleTests(TestCase):
class XYScopedRateThrottle(ScopedRateThrottle):
TIMER_SECONDS = 0
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
- timer = lambda self: self.TIMER_SECONDS
+
+ def timer(self):
+ return self.TIMER_SECONDS
class XView(APIView):
throttle_classes = (XYScopedRateThrottle,)
@@ -244,3 +284,70 @@ class ScopedRateThrottleTests(TestCase):
self.increment_timer()
response = self.unscoped_view(request)
self.assertEqual(200, response.status_code)
+
+
+class XffTestingBase(TestCase):
+ def setUp(self):
+
+ class Throttle(ScopedRateThrottle):
+ THROTTLE_RATES = {'test_limit': '1/day'}
+ TIMER_SECONDS = 0
+
+ def timer(self):
+ return self.TIMER_SECONDS
+
+ class View(APIView):
+ throttle_classes = (Throttle,)
+ throttle_scope = 'test_limit'
+
+ def get(self, request):
+ return Response('test_limit')
+
+ cache.clear()
+ self.throttle = Throttle()
+ self.view = View.as_view()
+ self.request = APIRequestFactory().get('/some_uri')
+ self.request.META['REMOTE_ADDR'] = '3.3.3.3'
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 2.2.2.2'
+
+ def config_proxy(self, num_proxies):
+ setattr(api_settings, 'NUM_PROXIES', num_proxies)
+
+
+class IdWithXffBasicTests(XffTestingBase):
+ def test_accepts_request_under_limit(self):
+ self.config_proxy(0)
+ self.assertEqual(200, self.view(self.request).status_code)
+
+ def test_denies_request_over_limit(self):
+ self.config_proxy(0)
+ self.view(self.request)
+ self.assertEqual(429, self.view(self.request).status_code)
+
+
+class XffSpoofingTests(XffTestingBase):
+ def test_xff_spoofing_doesnt_change_machine_id_with_one_app_proxy(self):
+ self.config_proxy(1)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 5.5.5.5, 2.2.2.2'
+ self.assertEqual(429, self.view(self.request).status_code)
+
+ def test_xff_spoofing_doesnt_change_machine_id_with_two_app_proxies(self):
+ self.config_proxy(2)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 1.1.1.1, 2.2.2.2'
+ self.assertEqual(429, self.view(self.request).status_code)
+
+
+class XffUniqueMachinesTest(XffTestingBase):
+ def test_unique_clients_are_counted_independently_with_one_proxy(self):
+ self.config_proxy(1)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 7.7.7.7'
+ self.assertEqual(200, self.view(self.request).status_code)
+
+ def test_unique_clients_are_counted_independently_with_two_proxies(self):
+ self.config_proxy(2)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
+ self.assertEqual(200, self.view(self.request).status_code)
diff --git a/rest_framework/tests/test_urlpatterns.py b/tests/test_urlpatterns.py
similarity index 98%
rename from rest_framework/tests/test_urlpatterns.py
rename to tests/test_urlpatterns.py
index 8132ec4c8..e0060e690 100644
--- a/rest_framework/tests/test_urlpatterns.py
+++ b/tests/test_urlpatterns.py
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from collections import namedtuple
+from django.conf.urls import patterns, url, include
from django.core import urlresolvers
from django.test import TestCase
from rest_framework.test import APIRequestFactory
-from rest_framework.compat import patterns, url, include
from rest_framework.urlpatterns import format_suffix_patterns
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 000000000..8c286ea42
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,166 @@
+from __future__ import unicode_literals
+from django.core.exceptions import ImproperlyConfigured
+from django.conf.urls import patterns, url
+from django.test import TestCase
+from django.utils import six
+from rest_framework.utils.model_meta import _resolve_model
+from rest_framework.utils.breadcrumbs import get_breadcrumbs
+from rest_framework.views import APIView
+from tests.models import BasicModel
+
+import rest_framework.utils.model_meta
+
+
+class Root(APIView):
+ pass
+
+
+class ResourceRoot(APIView):
+ pass
+
+
+class ResourceInstance(APIView):
+ pass
+
+
+class NestedResourceRoot(APIView):
+ pass
+
+
+class NestedResourceInstance(APIView):
+ pass
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', Root.as_view()),
+ url(r'^resource/$', ResourceRoot.as_view()),
+ url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()),
+ url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()),
+ url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()),
+)
+
+
+class BreadcrumbTests(TestCase):
+ """
+ Tests the breadcrumb functionality used by the HTML renderer.
+ """
+ urls = 'tests.test_utils'
+
+ def test_root_breadcrumbs(self):
+ url = '/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+ def test_resource_root_breadcrumbs(self):
+ url = '/resource/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/')
+ ]
+ )
+
+ def test_resource_instance_breadcrumbs(self):
+ url = '/resource/123'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123')
+ ]
+ )
+
+ def test_nested_resource_breadcrumbs(self):
+ url = '/resource/123/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/')
+ ]
+ )
+
+ def test_nested_resource_instance_breadcrumbs(self):
+ url = '/resource/123/abc'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/'),
+ ('Nested Resource Instance', '/resource/123/abc')
+ ]
+ )
+
+ def test_broken_url_breadcrumbs_handled_gracefully(self):
+ url = '/foobar'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+
+class ResolveModelTests(TestCase):
+ """
+ `_resolve_model` should return a Django model class given the
+ provided argument is a Django model class itself, or a properly
+ formatted string representation of one.
+ """
+ def test_resolve_django_model(self):
+ resolved_model = _resolve_model(BasicModel)
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_string_representation(self):
+ resolved_model = _resolve_model('tests.BasicModel')
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_non_django_model(self):
+ with self.assertRaises(ValueError):
+ _resolve_model(TestCase)
+
+ def test_resolve_improper_string_representation(self):
+ with self.assertRaises(ValueError):
+ _resolve_model('BasicModel')
+
+
+class ResolveModelWithPatchedDjangoTests(TestCase):
+ """
+ Test coverage for when Django's `get_model` returns `None`.
+
+ Under certain circumstances Django may return `None` with `get_model`:
+ http://git.io/get-model-source
+
+ It usually happens with circular imports so it is important that DRF
+ excepts early, otherwise fault happens downstream and is much more
+ difficult to debug.
+
+ """
+
+ def setUp(self):
+ """Monkeypatch get_model."""
+ self.get_model = rest_framework.utils.model_meta.models.get_model
+
+ def get_model(app_label, model_name):
+ return None
+
+ rest_framework.utils.model_meta.models.get_model = get_model
+
+ def tearDown(self):
+ """Revert monkeypatching."""
+ rest_framework.utils.model_meta.models.get_model = self.get_model
+
+ def test_blows_up_if_model_does_not_resolve(self):
+ with self.assertRaises(ImproperlyConfigured):
+ _resolve_model('tests.BasicModel')
diff --git a/tests/test_validation.py b/tests/test_validation.py
new file mode 100644
index 000000000..4234efd36
--- /dev/null
+++ b/tests/test_validation.py
@@ -0,0 +1,183 @@
+from __future__ import unicode_literals
+from django.core.validators import RegexValidator, MaxValueValidator
+from django.db import models
+from django.test import TestCase
+from rest_framework import generics, serializers, status
+from rest_framework.test import APIRequestFactory
+import re
+
+factory = APIRequestFactory()
+
+
+# Regression for #666
+
+class ValidationModel(models.Model):
+ blank_validated_field = models.CharField(max_length=255)
+
+
+class ValidationModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ValidationModel
+ fields = ('blank_validated_field',)
+ read_only_fields = ('blank_validated_field',)
+
+
+class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ValidationModel.objects.all()
+ serializer_class = ValidationModelSerializer
+
+
+# Regression for #653
+
+class ShouldValidateModel(models.Model):
+ should_validate_field = models.CharField(max_length=255)
+
+
+class ShouldValidateModelSerializer(serializers.ModelSerializer):
+ renamed = serializers.CharField(source='should_validate_field', required=False)
+
+ def validate_renamed(self, value):
+ if len(value) < 3:
+ raise serializers.ValidationError('Minimum 3 characters.')
+ return value
+
+ class Meta:
+ model = ShouldValidateModel
+ fields = ('renamed',)
+
+
+class TestPreSaveValidationExclusionsSerializer(TestCase):
+ def test_renamed_fields_are_model_validated(self):
+ """
+ Ensure fields with 'source' applied do get still get model validation.
+ """
+ # We've set `required=False` on the serializer, but the model
+ # does not have `blank=True`, so this serializer should not validate.
+ serializer = ShouldValidateModelSerializer(data={'renamed': ''})
+ self.assertEqual(serializer.is_valid(), False)
+ self.assertIn('renamed', serializer.errors)
+ self.assertNotIn('should_validate_field', serializer.errors)
+
+
+class TestCustomValidationMethods(TestCase):
+ def test_custom_validation_method_is_executed(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'})
+ self.assertFalse(serializer.is_valid())
+ self.assertIn('renamed', serializer.errors)
+
+ def test_custom_validation_method_passing(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'})
+ self.assertTrue(serializer.is_valid())
+
+
+class ValidationSerializer(serializers.Serializer):
+ foo = serializers.CharField()
+
+ def validate_foo(self, attrs, source):
+ raise serializers.ValidationError("foo invalid")
+
+ def validate(self, attrs):
+ raise serializers.ValidationError("serializer invalid")
+
+
+class TestAvoidValidation(TestCase):
+ """
+ If serializer was initialized with invalid data (None or non dict-like), it
+ should avoid validation layer (validate_ and validate methods)
+ """
+ def test_serializer_errors_has_only_invalid_data_error(self):
+ serializer = ValidationSerializer(data='invalid data')
+ self.assertFalse(serializer.is_valid())
+ self.assertDictEqual(serializer.errors, {
+ 'non_field_errors': [
+ 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__
+ ]
+ })
+
+
+# regression tests for issue: 1493
+
+class ValidationMaxValueValidatorModel(models.Model):
+ number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)])
+
+
+class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ValidationMaxValueValidatorModel
+
+
+class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ValidationMaxValueValidatorModel.objects.all()
+ serializer_class = ValidationMaxValueValidatorModelSerializer
+
+
+class TestMaxValueValidatorValidation(TestCase):
+
+ def test_max_value_validation_serializer_success(self):
+ serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99})
+ self.assertTrue(serializer.is_valid())
+
+ def test_max_value_validation_serializer_fails(self):
+ serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101})
+ self.assertFalse(serializer.is_valid())
+ self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors)
+
+ def test_max_value_validation_success(self):
+ obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
+ request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json')
+ view = UpdateMaxValueValidationModel().as_view()
+ response = view(request, pk=obj.pk).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_max_value_validation_fail(self):
+ obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
+ request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
+ view = UpdateMaxValueValidationModel().as_view()
+ response = view(request, pk=obj.pk).render()
+ self.assertEqual(response.content, b'{"number_value":["Ensure this value is less than or equal to 100."]}')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestChoiceFieldChoicesValidate(TestCase):
+ CHOICES = [
+ (0, 'Small'),
+ (1, 'Medium'),
+ (2, 'Large'),
+ ]
+
+ CHOICES_NESTED = [
+ ('Category', (
+ (1, 'First'),
+ (2, 'Second'),
+ (3, 'Third'),
+ )),
+ (4, 'Fourth'),
+ ]
+
+ def test_choices(self):
+ """
+ Make sure a value for choices works as expected.
+ """
+ f = serializers.ChoiceField(choices=self.CHOICES)
+ value = self.CHOICES[0][0]
+ try:
+ f.to_internal_value(value)
+ except serializers.ValidationError:
+ self.fail("Value %s does not validate" % str(value))
+
+
+class RegexSerializer(serializers.Serializer):
+ pin = serializers.CharField(
+ validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'),
+ message='A PIN is 4-6 digits')])
+
+expected_repr = """
+RegexSerializer():
+ pin = CharField(validators=[])
+""".strip()
+
+
+class TestRegexSerializer(TestCase):
+ def test_regex_repr(self):
+ serializer_repr = repr(RegexSerializer())
+ assert serializer_repr == expected_repr
diff --git a/tests/test_validators.py b/tests/test_validators.py
new file mode 100644
index 000000000..127ec6f8b
--- /dev/null
+++ b/tests/test_validators.py
@@ -0,0 +1,347 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+import datetime
+
+
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Tests for `UniqueValidator`
+# ---------------------------
+
+class UniquenessModel(models.Model):
+ username = models.CharField(unique=True, max_length=100)
+
+
+class UniquenessSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessModel
+
+
+class AnotherUniquenessModel(models.Model):
+ code = models.IntegerField(unique=True)
+
+
+class AnotherUniquenessSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AnotherUniquenessModel
+
+
+class TestUniquenessValidation(TestCase):
+ def setUp(self):
+ self.instance = UniquenessModel.objects.create(username='existing')
+
+ def test_repr(self):
+ serializer = UniquenessSerializer()
+ expected = dedent("""
+ UniquenessSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ username = CharField(max_length=100, validators=[])
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique(self):
+ data = {'username': 'existing'}
+ serializer = UniquenessSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'username': ['This field must be unique.']}
+
+ def test_is_unique(self):
+ data = {'username': 'other'}
+ serializer = UniquenessSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'other'}
+
+ def test_updated_instance_excluded(self):
+ data = {'username': 'existing'}
+ serializer = UniquenessSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'existing'}
+
+ def test_doesnt_pollute_model(self):
+ instance = AnotherUniquenessModel.objects.create(code='100')
+ serializer = AnotherUniquenessSerializer(instance)
+ self.assertEqual(
+ AnotherUniquenessModel._meta.get_field('code').validators, [])
+
+ # Accessing data shouldn't effect validators on the model
+ serializer.data
+ self.assertEqual(
+ AnotherUniquenessModel._meta.get_field('code').validators, [])
+
+
+# Tests for `UniqueTogetherValidator`
+# -----------------------------------
+
+class UniquenessTogetherModel(models.Model):
+ race_name = models.CharField(max_length=100)
+ position = models.IntegerField()
+
+ class Meta:
+ unique_together = ('race_name', 'position')
+
+
+class NullUniquenessTogetherModel(models.Model):
+ """
+ Used to ensure that null values are not included when checking
+ unique_together constraints.
+
+ Ignoring items which have a null in any of the validated fields is the same
+ behavior that database backends will use when they have the
+ unique_together constraint added.
+
+ Example case: a null position could indicate a non-finisher in the race,
+ there could be many non-finishers in a race, but all non-NULL
+ values *should* be unique against the given `race_name`.
+ """
+ date_of_birth = models.DateField(null=True) # Not part of the uniqueness constraint
+ race_name = models.CharField(max_length=100)
+ position = models.IntegerField(null=True)
+
+ class Meta:
+ unique_together = ('race_name', 'position')
+
+
+class UniquenessTogetherSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+
+
+class NullUniquenessTogetherSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullUniquenessTogetherModel
+
+
+class TestUniquenessTogetherValidation(TestCase):
+ def setUp(self):
+ self.instance = UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=1
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=2
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='other',
+ position=1
+ )
+
+ def test_repr(self):
+ serializer = UniquenessTogetherSerializer()
+ expected = dedent("""
+ UniquenessTogetherSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100, required=True)
+ position = IntegerField(required=True)
+ class Meta:
+ validators = []
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique_together(self):
+ """
+ Failing unique together validation should result in non field errors.
+ """
+ data = {'race_name': 'example', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'non_field_errors': [
+ 'The fields race_name, position must make a unique set.'
+ ]
+ }
+
+ def test_is_unique_together(self):
+ """
+ In a unique together validation, one field may be non-unique
+ so long as the set as a whole is unique.
+ """
+ data = {'race_name': 'other', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'other',
+ 'position': 2
+ }
+
+ def test_updated_instance_excluded_from_unique_together(self):
+ """
+ When performing an update, the existing instance does not count
+ as a match against uniqueness.
+ """
+ data = {'race_name': 'example', 'position': 1}
+ serializer = UniquenessTogetherSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'example',
+ 'position': 1
+ }
+
+ def test_unique_together_is_required(self):
+ """
+ In a unique together validation, all fields are required.
+ """
+ data = {'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data, partial=True)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'race_name': ['This field is required.']
+ }
+
+ def test_ignore_excluded_fields(self):
+ """
+ When model fields are not included in a serializer, then uniqueness
+ validators should not be added for that field.
+ """
+ class ExcludedFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+ fields = ('id', 'race_name',)
+ serializer = ExcludedFieldSerializer()
+ expected = dedent("""
+ ExcludedFieldSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100)
+ """)
+ assert repr(serializer) == expected
+
+ def test_ignore_validation_for_null_fields(self):
+ # None values that are on fields which are part of the uniqueness
+ # constraint cause the instance to ignore uniqueness validation.
+ NullUniquenessTogetherModel.objects.create(
+ date_of_birth=datetime.date(2000, 1, 1),
+ race_name='Paris Marathon',
+ position=None
+ )
+ data = {
+ 'date': datetime.date(2000, 1, 1),
+ 'race_name': 'Paris Marathon',
+ 'position': None
+ }
+ serializer = NullUniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid()
+
+ def test_do_not_ignore_validation_for_null_fields(self):
+ # None values that are not on fields part of the uniqueness constraint
+ # do not cause the instance to skip validation.
+ NullUniquenessTogetherModel.objects.create(
+ date_of_birth=datetime.date(2000, 1, 1),
+ race_name='Paris Marathon',
+ position=1
+ )
+ data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1}
+ serializer = NullUniquenessTogetherSerializer(data=data)
+ assert not serializer.is_valid()
+
+
+# Tests for `UniqueForDateValidator`
+# ----------------------------------
+
+class UniqueForDateModel(models.Model):
+ slug = models.CharField(max_length=100, unique_for_date='published')
+ published = models.DateField()
+
+
+class UniqueForDateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniqueForDateModel
+
+
+class TestUniquenessForDateValidation(TestCase):
+ def setUp(self):
+ self.instance = UniqueForDateModel.objects.create(
+ slug='existing',
+ published='2000-01-01'
+ )
+
+ def test_repr(self):
+ serializer = UniqueForDateSerializer()
+ expected = dedent("""
+ UniqueForDateSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = DateField(required=True)
+ class Meta:
+ validators = []
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique_for_date(self):
+ """
+ Failing unique for date validation should result in field error.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-01'}
+ serializer = UniqueForDateSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'slug': ['This field must be unique for the "published" date.']
+ }
+
+ def test_is_unique_for_date(self):
+ """
+ Passing unique for date validation.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-02'}
+ serializer = UniqueForDateSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'slug': 'existing',
+ 'published': datetime.date(2000, 1, 2)
+ }
+
+ def test_updated_instance_excluded_from_unique_for_date(self):
+ """
+ When performing an update, the existing instance does not count
+ as a match against unique_for_date.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-01'}
+ serializer = UniqueForDateSerializer(instance=self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'slug': 'existing',
+ 'published': datetime.date(2000, 1, 1)
+ }
+
+
+class HiddenFieldUniqueForDateModel(models.Model):
+ slug = models.CharField(max_length=100, unique_for_date='published')
+ published = models.DateTimeField(auto_now_add=True)
+
+
+class TestHiddenFieldUniquenessForDateValidation(TestCase):
+ def test_repr_date_field_not_included(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = HiddenFieldUniqueForDateModel
+ fields = ('id', 'slug')
+
+ serializer = TestSerializer()
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = HiddenField(default=CreateOnlyDefault())
+ class Meta:
+ validators = []
+ """)
+ assert repr(serializer) == expected
+
+ def test_repr_date_field_included(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = HiddenFieldUniqueForDateModel
+ fields = ('id', 'slug', 'published')
+
+ serializer = TestSerializer()
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = DateTimeField(default=CreateOnlyDefault(), read_only=True)
+ class Meta:
+ validators = []
+ """)
+ assert repr(serializer) == expected
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
new file mode 100644
index 000000000..90ad8afd2
--- /dev/null
+++ b/tests/test_versioning.py
@@ -0,0 +1,264 @@
+from .utils import UsingURLPatterns
+from django.conf.urls import include, url
+from rest_framework import serializers
+from rest_framework import status, versioning
+from rest_framework.decorators import APIView
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework.test import APIRequestFactory, APITestCase
+from rest_framework.versioning import NamespaceVersioning
+import pytest
+
+
+class RequestVersionView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+
+class ReverseView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'url': reverse('another', request=request)})
+
+
+class RequestInvalidVersionView(APIView):
+ def determine_version(self, request, *args, **kwargs):
+ scheme = self.versioning_class()
+ scheme.allowed_versions = ('v1', 'v2')
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+
+factory = APIRequestFactory()
+
+
+def dummy_view(request):
+ pass
+
+
+def dummy_pk_view(request, pk):
+ pass
+
+
+class TestRequestVersion:
+ def test_unversioned(self):
+ view = RequestVersionView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/1.2.3/endpoint/')
+ response = view(request, version='1.2.3')
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+
+class TestURLReversing(UsingURLPatterns, APITestCase):
+ included = [
+ url(r'^namespaced/$', dummy_view, name='another'),
+ url(r'^example/(?P\d+)/$', dummy_pk_view, name='example-detail')
+ ]
+
+ urlpatterns = [
+ url(r'^v1/', include(included, namespace='v1')),
+ url(r'^another/$', dummy_view, name='another'),
+ url(r'^(?P[^/]+)/another/$', dummy_view, name='another'),
+ ]
+
+ def test_reverse_unversioned(self):
+ view = ReverseView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v1')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/?version=v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'url': 'http://v1.example.org/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/namespaced/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+
+class TestInvalidVersion:
+ def test_invalid_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v3.example.org')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
+
+ def test_invalid_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v3'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+
+class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase):
+ included = [
+ url(r'^namespaced/(?P\d+)/$', dummy_view, name='namespaced'),
+ ]
+
+ urlpatterns = [
+ url(r'^v1/', include(included, namespace='v1')),
+ url(r'^v2/', include(included, namespace='v2'))
+ ]
+
+ def setUp(self):
+ super(TestHyperlinkedRelatedField, self).setUp()
+
+ class MockQueryset(object):
+ def get(self, pk):
+ return 'object %s' % pk
+
+ self.field = serializers.HyperlinkedRelatedField(
+ view_name='namespaced',
+ queryset=MockQueryset()
+ )
+ request = factory.get('/')
+ request.versioning_scheme = NamespaceVersioning()
+ request.version = 'v1'
+ self.field._context = {'request': request}
+
+ def test_bug_2489(self):
+ assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3'
+ with pytest.raises(serializers.ValidationError):
+ self.field.to_internal_value('/v2/namespaced/3/')
diff --git a/rest_framework/tests/test_views.py b/tests/test_views.py
similarity index 67%
rename from rest_framework/tests/test_views.py
rename to tests/test_views.py
index c0bec5aed..77b113ee5 100644
--- a/rest_framework/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+import sys
import copy
from django.test import TestCase
from rest_framework import status
@@ -11,6 +12,11 @@ from rest_framework.views import APIView
factory = APIRequestFactory()
+if sys.version_info[:2] >= (3, 4):
+ JSON_ERROR = 'JSON parse error - Expecting value:'
+else:
+ JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
+
class BasicView(APIView):
def get(self, request, *args, **kwargs):
@@ -32,13 +38,23 @@ def basic_view(request):
return {'method': 'PATCH', 'data': request.DATA}
+class ErrorView(APIView):
+ def get(self, request, *args, **kwargs):
+ raise Exception
+
+
+@api_view(['GET'])
+def error_view(request):
+ raise Exception
+
+
def sanitise_json_error(error_dict):
"""
Exact contents of JSON error messages depend on the installed version
of json.
"""
ret = copy.copy(error_dict)
- chop = len('JSON parse error - No JSON object could be decoded')
+ chop = len(JSON_ERROR)
ret['detail'] = ret['detail'][:chop]
return ret
@@ -51,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -66,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -80,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -95,7 +111,38 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
+
+
+class TestCustomExceptionHandler(TestCase):
+ def setUp(self):
+ self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
+
+ def exception_handler(exc):
+ return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
+
+ api_settings.EXCEPTION_HANDLER = exception_handler
+
+ def tearDown(self):
+ api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
+
+ def test_class_based_view_exception_handler(self):
+ view = ErrorView.as_view()
+
+ request = factory.get('/', content_type='application/json')
+ response = view(request)
+ expected = 'Error!'
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data, expected)
+
+ def test_function_based_view_exception_handler(self):
+ view = error_view
+
+ request = factory.get('/', content_type='application/json')
+ response = view(request)
+ expected = 'Error!'
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data, expected)
diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py
new file mode 100644
index 000000000..4d18a955d
--- /dev/null
+++ b/tests/test_viewsets.py
@@ -0,0 +1,35 @@
+from django.test import TestCase
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.test import APIRequestFactory
+from rest_framework.viewsets import GenericViewSet
+
+
+factory = APIRequestFactory()
+
+
+class BasicViewSet(GenericViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'ACTION': 'LIST'})
+
+
+class InitializeViewSetsTestCase(TestCase):
+ def test_initialize_view_set_with_actions(self):
+ request = factory.get('/', '', content_type='application/json')
+ my_view = BasicViewSet.as_view(actions={
+ 'get': 'list',
+ })
+
+ response = my_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, {'ACTION': 'LIST'})
+
+ def test_initialize_view_set_with_empty_actions(self):
+ try:
+ BasicViewSet.as_view()
+ except TypeError as e:
+ self.assertEqual(str(e), "The `actions` argument must be provided "
+ "when calling `.as_view()` on a ViewSet. "
+ "For example `.as_view({'get': 'list'})`")
+ else:
+ self.fail("actions must not be empty.")
diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py
new file mode 100644
index 000000000..dd3bbd6e1
--- /dev/null
+++ b/tests/test_write_only_fields.py
@@ -0,0 +1,31 @@
+from django.test import TestCase
+from rest_framework import serializers
+
+
+class WriteOnlyFieldTests(TestCase):
+ def setUp(self):
+ class ExampleSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ password = serializers.CharField(write_only=True)
+
+ def create(self, attrs):
+ return attrs
+
+ self.Serializer = ExampleSerializer
+
+ def write_only_fields_are_present_on_input(self):
+ data = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.validated_data, data)
+
+ def write_only_fields_are_not_present_on_output(self):
+ instance = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(instance)
+ self.assertEquals(serializer.data, {'email': 'foo@example.com'})
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 000000000..41f527dfd
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,6 @@
+"""
+Blank URLConf just to keep the test suite happy
+"""
+from django.conf.urls import patterns
+
+urlpatterns = patterns('')
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 000000000..b90349967
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,77 @@
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import NoReverseMatch
+
+
+class UsingURLPatterns(object):
+ """
+ Isolates URL patterns used during testing on the test class itself.
+ For example:
+
+ class MyTestCase(UsingURLPatterns, TestCase):
+ urlpatterns = [
+ ...
+ ]
+
+ def test_something(self):
+ ...
+ """
+ urls = __name__
+
+ def setUp(self):
+ global urlpatterns
+ urlpatterns = self.urlpatterns
+
+ def tearDown(self):
+ global urlpatterns
+ urlpatterns = []
+
+
+class MockObject(object):
+ def __init__(self, **kwargs):
+ self._kwargs = kwargs
+ for key, val in kwargs.items():
+ setattr(self, key, val)
+
+ def __str__(self):
+ kwargs_str = ', '.join([
+ '%s=%s' % (key, value)
+ for key, value in sorted(self._kwargs.items())
+ ])
+ return '' % kwargs_str
+
+
+class MockQueryset(object):
+ def __init__(self, iterable):
+ self.items = iterable
+
+ def get(self, **lookup):
+ for item in self.items:
+ if all([
+ getattr(item, key, None) == value
+ for key, value in lookup.items()
+ ]):
+ return item
+ raise ObjectDoesNotExist()
+
+
+class BadType(object):
+ """
+ When used as a lookup with a `MockQueryset`, these objects
+ will raise a `TypeError`, as occurs in Django when making
+ queryset lookups with an incorrect type for the lookup value.
+ """
+ def __eq__(self):
+ raise TypeError()
+
+
+def mock_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ args = args or []
+ kwargs = kwargs or {}
+ value = (args + list(kwargs.values()) + ['-'])[0]
+ prefix = 'http://example.org' if request else ''
+ suffix = ('.' + format) if (format is not None) else ''
+ return '%s/%s/%s%s/' % (prefix, view_name, value, suffix)
+
+
+def fail_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ raise NoReverseMatch()
diff --git a/tox.ini b/tox.ini
index aa97fd350..c986250c5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,102 +1,31 @@
[tox]
-downloadcache = {toxworkdir}/cache/
-envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
+envlist =
+ py27-{flake8,docs},
+ {py26,py27}-django14,
+ {py26,py27,py32,py33,py34}-django{15,16},
+ {py27,py32,py33,py34}-django{17,18beta}
[testenv]
-commands = {envpython} rest_framework/runtests/runtests.py
+commands = ./runtests.py --fast
+setenv =
+ PYTHONDONTWRITEBYTECODE=1
+deps =
+ django14: Django==1.4.11 # Should track minimum supported
+ django15: Django==1.5.6 # Should track minimum supported
+ django16: Django==1.6.3 # Should track minimum supported
+ django17: Django==1.7.2 # Should track maximum supported
+ django18beta: https://www.djangoproject.com/download/1.8b1/tarball/
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-optionals.txt
-[testenv:py3.3-django1.6]
-basepython = python3.3
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
+[testenv:py27-flake8]
+deps =
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-codestyle.txt
+commands = ./runtests.py --lintonly
-[testenv:py3.2-django1.6]
-basepython = python3.2
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py2.7-django1.6]
-basepython = python2.7
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
-
-[testenv:py2.6-django1.6]
-basepython = python2.6
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
-
-[testenv:py3.3-django1.5]
-basepython = python3.3
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py3.2-django1.5]
-basepython = python3.2
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py2.7-django1.5]
-basepython = python2.7
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.5]
-basepython = python2.6
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.7-django1.4]
-basepython = python2.7
-deps = django==1.4.3
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.4]
-basepython = python2.6
-deps = django==1.4.3
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.7-django1.3]
-basepython = python2.7
-deps = django==1.3.5
- django-filter==0.5.4
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.3]
-basepython = python2.6
-deps = django==1.3.5
- django-filter==0.5.4
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
+[testenv:py27-docs]
+deps =
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-documentation.txt
+commands = mkdocs build
',item:'