diff options
Diffstat (limited to 'rest_framework')
187 files changed, 15100 insertions, 16203 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 776618ac..f8bbeee3 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 cf001a24..f0702286 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 d5965e40..769f6202 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 rest_framework.settings import api_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(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'] +from __future__ import unicode_literals + +from django.db import models, migrations +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + 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 7601f5b7..a1a9315f 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 60a3740e..37ade255 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 00000000..5b927f3e --- /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 index e69de29b..e69de29b 100644 --- a/rest_framework/runtests/__init__.py +++ b/rest_framework/authtoken/south_migrations/__init__.py diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index 7c03cb76..b75c2e25 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 6f7447ad..c6a4a869 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 - -# Try to import six from Django, fallback to included `six`. +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: - from django.utils import six + import importlib except ImportError: - from rest_framework import six + from django.utils import importlib -# 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 +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) -# Handle django.utils.encoding rename: -# smart_unicode -> smart_text -# force_unicode -> force_text -try: - from django.utils.encoding import smart_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_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 force_text + from collections import OrderedDict 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 -# django-filter is optional -try: - import django_filters -except ImportError: - django_filters = None - -# cStringIO only if it's available, otherwise StringIO +# contrib.postgres only supported from 1.8 onwards. try: - import cStringIO.StringIO as StringIO + from django.contrib.postgres import fields as postgres_fields except ImportError: - StringIO = six.StringIO - -BytesIO = six.BytesIO + postgres_fields = None -# urlparse compat import (Required because it changed in python 3.x) -try: - from urllib import parse as urlparse -except ImportError: - import urlparse +# 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) -# Try to import PIL in either of the two ways it can end up installed. +# django-filter is optional try: - from PIL import Image + import django_filters except ImportError: + django_filters = None + +if django.VERSION >= (1, 6): + def clean_manytomany_helptext(text): + return text +else: + # 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 Image + import guardian + import guardian.shortcuts # Fixes #1624 except ImportError: - Image = None + pass -def get_concrete_model(model_cls): +def get_model_name(model_cls): try: - return model_cls._meta.concrete_model + return model_cls._meta.model_name 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 -else: - AUTH_USER_MODEL = 'auth.User' + # < 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 - - 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 + from django.views.generic import View as DjangoView + + class View(DjangoView): def _allowed_methods(self): return [m.upper() for m in self.http_method_names if hasattr(self, m)] -# 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 +# 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: - 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. + 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 - 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 + class URLValidator(DjangoURLValidator): + def __init__(self, *args, **kwargs): + self.message = kwargs.pop('message', self.message) + super(URLValidator, self).__init__(*args, **kwargs) -# 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<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$' - ) - - datetime_re = re.compile( - r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})' - r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})' - r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?' - r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$' - ) - - time_re = re.compile( - r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})' - r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\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)) +# 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 - # 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'!*\'();:@&=+$,/?#[]~') + class EmailValidator(DjangoEmailValidator): + def __init__(self, *args, **kwargs): + super(EmailValidator, self).__init__(email_re, *args, **kwargs) - return force_text(url) +# PATCH method is not implemented by Django +if 'patch' not in View.http_method_names: + View.http_method_names = View.http_method_names + ['patch'] -# 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 c69756a4..21de1acf 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. """ + methods = ['get'] if (methods is None) else methods + def decorator(func): - func.bind_to_methods = ['get'] + func.bind_to_methods = methods + func.detail = True func.kwargs = kwargs return func return decorator -def action(methods=['post'], **kwargs): +def list_route(methods=None, **kwargs): """ - Used to mark a method on a ViewSet that should be routed for POST requests. + 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 425a7214..f954c13e 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) -class ParseError(APIException): + 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 - default_detail = 'Malformed request.' - def __init__(self, detail=None): - self.detail = detail or self.default_detail + 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.') 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 = force_text(self.default_detail) + + if wait is None: + self.wait = None else: - self.detail = detail or self.default_detail + 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 f9931887..a80862e8 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,615 +52,861 @@ def is_simple_callable(obj): return len_args <= len_defaults -def get_component(obj, attr_name): - """ - Given an object, and an attribute name, - return that attribute on the object. +def get_attribute(instance, attrs): """ - if isinstance(obj, dict): - val = obj.get(attr_name) - else: - val = getattr(obj, attr_name) - - if is_simple_callable(val): - return val() - return val - - -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 - + Similar to Python's built in `getattr(instance, attr)`, + but takes a list of nested attributes, instead of a single attribute. -def strip_multiple_choice_msg(help_text): + Also accepts either attribute lookup on objects or dictionary lookups. """ - Remove the 'Hold down "control" ...' message that is Django enforces in - select multiple fields on ModelForms. (Required for 1.5 and earlier) + 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)) - See https://code.djangoproject.com/ticket/9321 - """ - multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.') - multiple_choice_msg = force_text(multiple_choice_msg) + return instance - return help_text.replace(multiple_choice_msg, '') +def set_value(dictionary, keys, value): + """ + Similar to Python's built in `dictionary[key] = value`, + but takes a list of nested keys instead of a single key. -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' - - def __init__(self, source=None, label=None, help_text=None): - self.parent = None + 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}} + """ + if not keys: + dictionary.update(value) + return - self.creation_counter = Field.creation_counter - Field.creation_counter += 1 + for key in keys[:-1]: + if key not in dictionary: + dictionary[key] = {} + dictionary = dictionary[key] - self.source = source + dictionary[keys[-1]] = value - 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)) +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 initialize(self, parent, field_name): - """ - Called to set up a field prior to field_to_native or field_from_native. + 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) - 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 __call__(self): + if self.is_update: + raise SkipField() + if callable(self.default): + return self.default() + return self.default - 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 __repr__(self): + return unicode_to_repr( + '%s(%s)' % (self.__class__.__name__, unicode_repr(self.default)) + ) - 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) +class CurrentUserDefault(object): + def set_context(self, serializer_field): + self.user = serializer_field.context['request'].user - source = self.source or field_name - value = obj + def __call__(self): + return self.user - for component in source.split('.'): - value = get_component(value, component) - if value is None: - break + def __repr__(self): + return unicode_to_repr('%s()' % self.__class__.__name__) - 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() +class SkipField(Exception): + pass - 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 {} +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.' +) - 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 Field(object): + _creation_counter = 0 -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__) + ) + + self.field_name = field_name + self.parent = parent - def __deepcopy__(self, memo): - result = copy.copy(self) - memo[id(self)] = result - result.validators = self.validators[:] - return result + # `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()) - def validate(self, value): - if value in validators.EMPTY_VALUES and self.required: - raise ValidationError(self.error_messages['required']) + 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 - - 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): + 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) + + @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', {}) -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") + 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 - 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)) + 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)) - 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)) +# Boolean types... - 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) +class BooleanField(Field): + default_error_messages = { + 'invalid': _('"{input}" is not a valid boolean.') + } + 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)) - 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 __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 attributes(self): - return { - "type": self.model_field.get_internal_type() - } + def to_internal_value(self, data): + if data in self.TRUE_VALUES: + return True + 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) -##### Typed Fields ##### -class BooleanField(WritableField): - type_name = 'BooleanField' - type_label = 'boolean' - form_field_class = forms.BooleanField - widget = widgets.CheckboxInput +class NullBooleanField(Field): default_error_messages = { - 'invalid': _("'%s' value must be either True or False."), + 'invalid': _('"{input}" is not a valid boolean.') } - empty = False + 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): + assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' + kwargs['allow_null'] = True + super(NullBooleanField, self).__init__(**kwargs) - # 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 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 from_native(self, value): - if value in ('true', 't', 'True', '1'): + def to_representation(self, value): + if value in self.NULL_VALUES: + return None + if value in self.TRUE_VALUES: return True - if value in ('false', 'f', 'False', '0'): + 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 +# String types... - 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)) +class CharField(Field): + default_error_messages = { + '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.') + } + initial = '' - def from_native(self, value): - if isinstance(value, six.string_types) or value is None: - return value - return smart_text(value) + 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 URLField(CharField): - type_name = 'URLField' - type_label = 'url' + def to_internal_value(self, data): + value = six.text_type(data) + return value.strip() if self.trim_whitespace else value + + def to_representation(self, value): + return six.text_type(value) + + +class EmailField(CharField): + default_error_messages = { + 'invalid': _('Enter a valid email address.') + } def __init__(self, **kwargs): - kwargs['validators'] = [validators.URLValidator()] - super(URLField, self).__init__(**kwargs) + super(EmailField, self).__init__(**kwargs) + validator = EmailValidator(message=self.error_messages['invalid']) + self.validators.append(validator) + + +class RegexField(CharField): + default_error_messages = { + 'invalid': _('This value does not match the required pattern.') + } + + 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): - type_name = 'SlugField' - type_label = 'slug' - form_field_class = forms.SlugField + 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 'slug' consisting of letters, numbers," - " underscores or hyphens."), + 'invalid': _('Enter a valid URL.') } - default_validators = [validators.validate_slug] - def __init__(self, *args, **kwargs): - super(SlugField, self).__init__(*args, **kwargs) + def __init__(self, **kwargs): + super(URLField, self).__init__(**kwargs) + validator = URLValidator(message=self.error_messages['invalid']) + self.validators.append(validator) -class ChoiceField(WritableField): - type_name = 'ChoiceField' - type_label = 'multiple choice' - form_field_class = forms.ChoiceField - widget = widgets.Select +class UUIDField(Field): default_error_messages = { - 'invalid_choice': _('Select a valid choice. %(value)s is not one of ' - 'the available choices.'), + 'invalid': _('"{value}" is not a valid UUID.'), } - 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 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 _get_choices(self): - return self._choices + def to_representation(self, value): + return str(value) - 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) +# Number types... - 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}) +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 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 __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') -class EmailField(CharField): - type_name = 'EmailField' - type_label = 'email' - form_field_class = forms.EmailField + 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': _('Enter a valid email address.'), + '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.') } - default_validators = [validators.validate_email] + MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs. - 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): + 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') -class RegexField(CharField): - type_name = 'RegexField' - type_label = 'regex' - form_field_class = forms.RegexField + 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') - 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 + try: + value = decimal.Decimal(data) + except decimal.DecimalException: + self.fail('invalid') - def _get_regex(self): - return self._regex + # 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') - 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) + # Check for infinity and negative infinity. + if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')): + self.fail('invalid') - regex = property(_get_regex, _set_regex) + 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) -class DateField(WritableField): - type_name = 'DateField' - type_label = 'date' - widget = widgets.DateInput - form_field_class = forms.DateField +# Date & time fields... +class DateTimeField(Field): default_error_messages = { - 'invalid': _("Date has wrong format. Use one of these formats instead: %s"), + 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'), + 'date': _('Expected a datetime but got a date.'), } - empty = None - input_formats = api_settings.DATE_INPUT_FORMATS - format = api_settings.DATE_FORMAT + 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, input_formats=None, format=None, *args, **kwargs): + 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() - if isinstance(value, datetime.date): - return value + return self.enforce_timezone(value) for format in self.input_formats: if format.lower() == ISO_8601: try: - parsed = parse_date(value) + parsed = parse_datetime(value) except (ValueError, TypeError): pass else: if parsed is not None: - return parsed + return self.enforce_timezone(parsed) else: try: parsed = datetime.datetime.strptime(value, format) except (ValueError, TypeError): pass else: - return parsed.date() + return self.enforce_timezone(parsed) - msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats) - raise ValidationError(msg) + humanized_format = humanize_datetime.datetime_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() - if self.format.lower() == ISO_8601: - return value.isoformat() + value = value.isoformat() + if value.endswith('+00:00'): + value = value[:-6] + 'Z' + return value return value.strftime(self.format) -class DateTimeField(WritableField): - type_name = 'DateTimeField' - type_label = 'datetime' - widget = widgets.DateTimeInput - form_field_class = forms.DateTimeField - +class DateField(Field): default_error_messages = { - 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"), + 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'), + 'datetime': _('Expected a date but got a datetime.'), } - empty = None - input_formats = api_settings.DATETIME_INPUT_FORMATS - format = api_settings.DATETIME_FORMAT + format = api_settings.DATE_FORMAT + input_formats = api_settings.DATE_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(DateTimeField, self).__init__(*args, **kwargs) - - def from_native(self, value): - if value in validators.EMPTY_VALUES: - return None + super(DateField, self).__init__(*args, **kwargs) + def to_internal_value(self, value): if isinstance(value, datetime.datetime): - return value + self.fail('datetime') + 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) + parsed = parse_date(value) except (ValueError, TypeError): pass else: @@ -664,45 +918,42 @@ class DateTimeField(WritableField): except (ValueError, TypeError): pass else: - return parsed + return parsed.date() - msg = self.error_messages['invalid'] % readable_datetime_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 + # 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: - ret = value.isoformat() - if ret.endswith('+00:00'): - ret = ret[:-6] + 'Z' - return ret + return value.isoformat() return value.strftime(self.format) -class TimeField(WritableField): - type_name = 'TimeField' - type_label = 'time' - widget = widgets.TimeInput - form_field_class = forms.TimeField - +class TimeField(Field): default_error_messages = { - 'invalid': _("Time 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.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) - - 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): - if value in validators.EMPTY_VALUES: - return None - - try: - value = int(str(value)) - except (ValueError, TypeError): - raise ValidationError(self.error_messages['invalid']) - return value + 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]) + # 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() + ]) -class FloatField(WritableField): - type_name = 'FloatField' - type_label = 'float' - form_field_class = forms.FloatField + self.allow_blank = kwargs.pop('allow_blank', False) - default_error_messages = { - 'invalid': _("'%s' value must be a float."), - } + super(ChoiceField, self).__init__(**kwargs) - def from_native(self, value): - if value in validators.EMPTY_VALUES: - return None + def to_internal_value(self, data): + if data == '' and self.allow_blank: + return '' try: - return float(value) - except (TypeError, ValueError): - msg = self.error_messages['invalid'] % value - raise ValidationError(msg) + 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 DecimalField(WritableField): - type_name = 'DecimalField' - type_label = 'decimal' - form_field_class = forms.DecimalField +class MultipleChoiceField(ChoiceField): 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.') + '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 __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) + 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) - 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 to_internal_value(self, data): + if isinstance(data, type('')) or not hasattr(data, '__iter__'): + self.fail('not_a_list', input_type=type(data).__name__) - 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 + return set([ + super(MultipleChoiceField, self).to_internal_value(item) + for item in data + ]) - 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 + def to_representation(self, value): + return set([ + self.choice_strings_to_values[six.text_type(item)] for item in value + ]) -class FileField(WritableField): - use_files = True - type_name = 'FileField' - type_label = 'file upload' - form_field_class = forms.FileField - widget = widgets.FileInput +# File types... +class FileField(Field): 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.' + ), + } + + 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 = { - 'invalid_image': _("Upload a valid image. The file you uploaded was " - "either not an image or a corrupted image."), + 'not_a_list': _('Expected a list of items but got type "{input_type}".') } - def from_native(self, data): + 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] + + 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] - from compat import Image - assert Image is not None, 'PIL must be installed for ImageField support' - # 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']) +class DictField(Field): + child = _UnvalidatedField() + initial = {} + default_error_messages = { + 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".') + } - 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 + 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() + ]) -class SerializerMethodField(Field): +# 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. """ - A field that gets its value by calling a method on the serializer it's attached to. + 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 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. - def __init__(self, method_name): + 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=None, **kwargs): self.method_name = method_name - super(SerializerMethodField, self).__init__() + kwargs['source'] = '*' + kwargs['read_only'] = True + super(SerializerMethodField, self).__init__(**kwargs) + + 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. - def field_to_native(self, obj, field_name): - value = getattr(self.parent, self.method_name)(obj) - return self.to_native(value) + 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 c058bc71..9a84efa2 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 99e9782e..61dcb84a 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,205 +24,25 @@ 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 - - ###################################### - # 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): - """ - Extra context provided to the serializer class. - """ - return { - 'request': self.request, - 'format': self.format_kwarg, - 'view': self - } - - def get_serializer(self, instance=None, data=None, - files=None, many=False, partial=False): - """ - 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 - - def get_serializer_class(self): - """ - Return the class to use for the serializer. - Defaults to using `self.serializer_class`. - - You may want to override this if you need to provide different - serializations depending on the incoming request. - - (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." \ - % self.__class__.__name__ - - class DefaultSerializer(self.model_serializer_class): - class Meta: - model = self.model - return DefaultSerializer + # The style to use for queryset pagination. + pagination_class = api_settings.DEFAULT_PAGINATION_CLASS def get_queryset(self): """ @@ -235,21 +50,28 @@ class GenericAPIView(views.APIView): This must be an iterable, and may be a queryset. Defaults to using `self.queryset`. + 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) """ - if self.queryset is not None: - return self.queryset._clone() - - if self.model is not None: - return self.model._default_manager.all() + assert self.queryset is not None, ( + "'%s' should either include a `queryset` attribute, " + "or override the `get_queryset()` method." + % self.__class__.__name__ + ) - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % 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, queryset=None): + def get_object(self): """ Returns the object the view is displaying. @@ -257,41 +79,19 @@ class GenericAPIView(views.APIView): queryset lookups. Eg if objects are referenced using multiple keyword arguments in the url conf. """ - # Determine the base queryset to use. - if queryset is None: - queryset = self.filter_queryset(self.get_queryset()) - else: - pass # Deprecation warning + queryset = self.filter_queryset(self.get_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) - ) + 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 @@ -299,68 +99,86 @@ class GenericAPIView(views.APIView): 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 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() + kwargs['context'] = self.get_serializer_context() + return serializer_class(*args, **kwargs) - def pre_save(self, obj): + def get_serializer_class(self): """ - Placeholder method for calling before saving an object. + Return the class to use for the serializer. + Defaults to using `self.serializer_class`. + + You may want to override this if you need to provide different + serializations depending on the incoming request. - May be used to set attributes on the object that are implicit - in either the request, or the url. + (Eg. admins get full serialization, others get basic serialization) """ - pass + 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__ + ) + + return self.serializer_class - def post_save(self, obj, created=False): + def get_serializer_context(self): """ - Placeholder method for calling after saving an object. + Extra context provided to the serializer class. + """ + return { + 'request': self.request, + 'format': self.format_kwarg, + 'view': self + } + + def filter_queryset(self, queryset): """ - pass + Given a queryset, filter it with whichever filter backend is in use. - def metadata(self, request): + 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. """ - Return a dictionary of metadata about the view. - Used to return responses for OPTIONS requests. + for backend in list(self.filter_backends): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset - We override the default behavior, and add some extra information - about the required request body for POST and PUT operations. + @property + def paginator(self): """ - 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 + The paginator instance associated with the view, or `None`. + """ + 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 Binary files differnew file mode 100644 index 00000000..fe1b676c --- /dev/null +++ b/rest_framework/locale/ar/LC_MESSAGES/django.mo 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 00000000..a910a7c9 --- /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 <d.eyad.t@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..a5e67713 --- /dev/null +++ b/rest_framework/locale/cs/LC_MESSAGES/django.mo 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 00000000..50e7034b --- /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 <Jirka.Vejrazka@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..f947f907 --- /dev/null +++ b/rest_framework/locale/da/LC_MESSAGES/django.mo 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 00000000..e00ffadf --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..48245c60 --- /dev/null +++ b/rest_framework/locale/de/LC_MESSAGES/django.mo 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 00000000..74bee416 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..746915ff --- /dev/null +++ b/rest_framework/locale/en/LC_MESSAGES/django.mo 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 00000000..f3db69e5 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..eb60d9d7 --- /dev/null +++ b/rest_framework/locale/en_US/LC_MESSAGES/django.mo 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 00000000..11d94e9c --- /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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\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 Binary files differnew file mode 100644 index 00000000..814db7be --- /dev/null +++ b/rest_framework/locale/es/LC_MESSAGES/django.mo 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 00000000..28ef893d --- /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 <jpadilla@webapplicate.com>, 2015 +# Miguel González <migonzalvar@activitycentral.com>, 2015 +# Sergio Infante <rsinfante@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..ca9b6ec4 --- /dev/null +++ b/rest_framework/locale/et/LC_MESSAGES/django.mo 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 00000000..dec03d4d --- /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 <tonis.kardi@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..68519d45 --- /dev/null +++ b/rest_framework/locale/fr/LC_MESSAGES/django.mo 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 00000000..e8597c30 --- /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é <etienne.desgagne@evimbec.ca>, 2015 +# Martin Maillard <martin.maillard@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..451b0b9a --- /dev/null +++ b/rest_framework/locale/hu/LC_MESSAGES/django.mo 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 00000000..14fb6544 --- /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 <defaultdict@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..7fc98bda --- /dev/null +++ b/rest_framework/locale/id/LC_MESSAGES/django.mo 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 00000000..99b70546 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..82ceb810 --- /dev/null +++ b/rest_framework/locale/it/LC_MESSAGES/django.mo 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 00000000..2cdfb877 --- /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 <promat85@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..457bb53c --- /dev/null +++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo 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 00000000..963fe89a --- /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 <best2378@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..fc586626 --- /dev/null +++ b/rest_framework/locale/mk/LC_MESSAGES/django.mo 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 00000000..d9a46953 --- /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 <filipdimitrovski22@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..b0e1ad77 --- /dev/null +++ b/rest_framework/locale/nl/LC_MESSAGES/django.mo 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 00000000..a1215512 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..9db72cfb --- /dev/null +++ b/rest_framework/locale/pl/LC_MESSAGES/django.mo 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 00000000..8e51d754 --- /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 <jh@blueice.pl>, 2015 +# Maciek Olko <maciej.olko@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..49f01929 --- /dev/null +++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo 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 00000000..3f272f71 --- /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 <masterjakul@gmail.com>, 2015 +# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..d1555f1f --- /dev/null +++ b/rest_framework/locale/ru/LC_MESSAGES/django.mo 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 00000000..38489747 --- /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 <mktums@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..53bb95d8 --- /dev/null +++ b/rest_framework/locale/sk/LC_MESSAGES/django.mo 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 00000000..9dd378c0 --- /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 <stano@videoflot.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..a33b0cc5 --- /dev/null +++ b/rest_framework/locale/sv/LC_MESSAGES/django.mo 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 00000000..1602bf55 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..e6b848cf --- /dev/null +++ b/rest_framework/locale/tr/LC_MESSAGES/django.mo 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 00000000..5aabbeba --- /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 <epaprat@gmail.com>, 2015 +# Mesut Can Gürle <mesutcang@gmail.com>, 2015 +# Recep KIRMIZI <rkirmizi@gmail.com>, 2015 +# Ülgen Sarıkavak <ulgensrkvk@gmail.com>, 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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..fc335054 --- /dev/null +++ b/rest_framework/locale/uk/LC_MESSAGES/django.mo 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 00000000..93fc2bf9 --- /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 <tom@tomchristie.com>\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 Binary files differnew file mode 100644 index 00000000..6e7073bd --- /dev/null +++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo 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 00000000..01128859 --- /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 <schemacs@gmail.com>, 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 <tom@tomchristie.com>\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 00000000..bf3611aa --- /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 f11def6d..c34cfcee 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()) - - # 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) + queryset = self.filter_queryset(self.get_queryset()) + + 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() - - 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) + 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) - 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 4d205c0e..1838130a 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 d51ea929..f41a9ae1 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' + ret = int(integer_string) + if ret < 0 or (ret == 0 and strict): + raise ValueError() + if cutoff: + ret = min(ret, cutoff) + return ret - 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) +def _divide_with_ceil(a, b): + """ + Returns 'a' divded by 'b', with any remainder rounded up. + """ + if a % b: + return (a // b) + 1 + return a // b -class PreviousPageField(serializers.Field): + +def _get_count(queryset): """ - Field that returns a link to the previous page in paginated results. + Determine an object count, supporting either querysets or regular lists. """ - page_field = 'page' + try: + return queryset.count() + except (AttributeError, TypeError): + return len(queryset) - 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) +def _get_displayed_page_numbers(current, final): + """ + This utility function determines a list of page numbers to display. + This gives us a nice contextually relevant set of page numbers. + + 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: -class DefaultObjectSerializer(serializers.Field): + https://gist.github.com/tomchristie/321140cebb1c4a558b15 """ - If no object serializer is specified, then this serializer will be applied - as the default. + 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 + + +def _get_page_links(page_numbers, current, url_func): """ + Given a list of page numbers and `None` page breaks, + return a list of `PageLink` objects. + """ + page_links = [] + for page_number in page_numbers: + if page_number is None: + page_link = PAGE_BREAK + else: + 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 - def __init__(self, source=None, context=None): - # Note: Swallow context kwarg - only required for eg. ModelSerializer. - super(DefaultObjectSerializer, self).__init__(source=source) + +def _decode_cursor(encoded): + """ + Given a string representing an encoded cursor, return a `Cursor` instance. + """ + + # 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) -class PaginationSerializerOptions(serializers.SerializerOptions): +def _encode_cursor(cursor): """ - An object that stores the options that may be provided to a - pagination serializer by using the inner `Meta` class. + 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') - Accessible on the instance as `serializer.opts`. + +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 __init__(self, meta): - super(PaginationSerializerOptions, self).__init__(meta) - self.object_serializer_class = getattr(meta, 'object_serializer_class', - DefaultObjectSerializer) + 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 BasePaginationSerializer(serializers.Serializer): +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 base class for pagination serializers to inherit from, - to make implementing custom serializers more easy. + 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 """ - _options_class = PaginationSerializerOptions - results_field = 'results' + # 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 - def __init__(self, *args, **kwargs): + last_page_strings = ('last',) + + template = 'rest_framework/pagination/numbers.html' + + invalid_page_message = _('Invalid page "{page_number}": {message}.') + + def _handle_backwards_compat(self, view): """ - Override init to add in the object serializer field on-the-fly. + 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. """ - super(BasePaginationSerializer, self).__init__(*args, **kwargs) - results_field = self.results_field - object_serializer = self.opts.object_serializer_class + 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." + ) - if 'context' in kwargs: - context_kwarg = {'context': kwargs['context']} - else: - context_kwarg = {} + 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 + } - self.fields[results_field] = object_serializer(source='object_list', **context_kwarg) + def to_html(self): + template = loader.get_template(self.template) + context = Context(self.get_html_context()) + return template.render(context) -class PaginationSerializer(BasePaginationSerializer): +class LimitOffsetPagination(BasePagination): """ - A default implementation of a pagination serializer. + 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): """ - count = serializers.Field(source='paginator.count') - next = NextPageField(source='*') - previous = PreviousPageField(source='*') + 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 96bfac84..437d1339 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 1036663e..9069d315 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: - return False + 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]] - 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) + def has_object_permission(self, request, view, obj): + model_cls = getattr(view, 'model', None) + queryset = getattr(view, 'queryset', None) - assert False, ('TokenHasReadWriteScope requires either the' - '`OAuthAuthentication` or `OAuth2Authentication` authentication ' - 'class to be used.') + 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 + + return True diff --git a/rest_framework/relations.py b/rest_framework/relations.py index edaf76d6..3a966c5b 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 - - cache_choices = False - empty_label = None - read_only = True - many = False - - def __init__(self, *args, **kwargs): - - # '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 - - kwargs['read_only'] = kwargs.pop('read_only', self.read_only) - super(RelatedField, self).__init__(*args, **kwargs) - - if not self.required: - self.empty_label = BLANK_CHOICE_DASH[0][1] - - self.queryset = queryset - - def initialize(self, parent, field_name): - super(RelatedField, self).initialize(parent, field_name) - if self.queryset is None and not self.read_only: - 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) - - ### We need this stuff to make form choices work... - - 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. + def __init__(self, pk): + self.pk = pk + + +# 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' +) + + +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) + + 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) + + @classmethod + def many_init(cls, *args, **kwargs): """ - 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) - - -### PrimaryKey relationships - -class PrimaryKeyRelatedField(RelatedField): - """ - Represents a relationship as a pk value. - """ - read_only = False + This method handles creating a parent `ManyRelatedField` instance + when the `many=True` keyword argument is passed. - default_error_messages = { - 'does_not_exist': _("Invalid pk '%s' - object does not exist."), - 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'), - } + Typically you won't need to override this method. - # TODO: Remove these field hacks... - def prepare_value(self, obj): - return self.to_native(obj.pk) + 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: - def label_from_instance(self, obj): - """ - Return a readable representation for use with eg. select widgets. + @classmethod + def many_init(cls, *args, **kwargs): + kwargs['child'] = cls() + return CustomManyRelatedField(*args, **kwargs) """ - 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 + 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 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 False - try: - return self.queryset.get(pk=data) - except ObjectDoesNotExist: - msg = self.error_messages['does_not_exist'] % smart_text(data) - raise ValidationError(msg) - except (TypeError, ValueError): - received = type(data).__name__ - msg = self.error_messages['incorrect_type'] % received - raise ValidationError(msg) - - def field_to_native(self, obj, field_name): - if self.many: - # To-many relationship - - 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) + 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: - pk = getattr(obj, self.source or field_name).pk - except ObjectDoesNotExist: - return None - - # Forward relationship - return self.to_native(pk) + instance = get_attribute(instance, self.source_attrs[:-1]) + return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1])) + except AttributeError: + pass + # Standard case, return the object instance. + return get_attribute(instance, self.source_attrs) -### Slug relationships + @property + def choices(self): + return OrderedDict([ + ( + six.text_type(self.to_representation(item)), + six.text_type(item) + ) + for item in self.queryset.all() + ]) -class SlugRelatedField(RelatedField): +class StringRelatedField(RelatedField): """ - Represents a relationship using a unique field on the target. + A read only field that represents its targets using their + plain string representation. """ - read_only = False - default_error_messages = { - 'does_not_exist': _("Object with %s=%s does not exist."), - 'invalid': _('Invalid value.'), - } + def __init__(self, **kwargs): + kwargs['read_only'] = True + super(StringRelatedField, self).__init__(**kwargs) - 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_representation(self, value): + return six.text_type(value) - 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') +class PrimaryKeyRelatedField(RelatedField): + default_error_messages = { + '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}.'), + } + + def use_pk_only_optimization(self): + return True + def to_internal_value(self, data): try: - return self.queryset.get(**{self.slug_field: data}) + return self.get_queryset().get(pk=data) except ObjectDoesNotExist: - raise ValidationError(self.error_messages['does_not_exist'] % - (self.slug_field, smart_text(data))) + self.fail('does_not_exist', pk_value=data) except (TypeError, ValueError): - msg = self.error_messages['invalid'] - raise ValidationError(msg) + self.fail('incorrect_type', data_type=type(data).__name__) + def to_representation(self, value): + return value.pk -### 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 - - 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 - - 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 + # Unsaved objects will not yet have a valid URL. + if obj.pk is None: + return None - raise NoReverseMatch() + 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) - 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) - except (ObjectDoesNotExist, TypeError, ValueError): - raise ValidationError(self.error_messages['does_not_exist']) - - -class HyperlinkedIdentityField(Field): - """ - Represents the instance, or a property on the instance, using hyperlinking. - """ - lookup_field = 'pk' - read_only = True + expected_viewname = request.versioning_scheme.get_versioned_viewname( + self.view_name, request + ) + except AttributeError: + expected_viewname = self.view_name - # These are all pending deprecation - pk_url_kwarg = 'pk' - slug_field = 'slug' - slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden + if match.view_name != expected_viewname: + self.fail('incorrect_match') - def __init__(self, *args, **kwargs): try: - self.view_name = kwargs.pop('view_name') - except KeyError: - msg = "HyperlinkedIdentityField requires 'view_name' argument" - raise ValueError(msg) + return self.get_object(match.view_name, match.args, match.kwargs) + except (ObjectDoesNotExist, TypeError, ValueError): + self.fail('does_not_exist') - 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} - try: - return reverse(view_name, kwargs=kwargs, request=request, format=format) - except NoReverseMatch: - pass +class HyperlinkedIdentityField(HyperlinkedRelatedField): + """ + A read-only field that represents the identity URL for an object, itself. - 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 + This is in contrast to `HyperlinkedRelatedField` which represents the + URL of relationships to other objects. + """ - 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 + 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) - raise NoReverseMatch() + 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 -### Old-style many classes for backwards compat +class SlugRelatedField(RelatedField): + """ + A read-write field the represents the target of the relationship + by a unique 'slug' attribute. + """ -class ManyRelatedField(RelatedField): - def __init__(self, *args, **kwargs): - warnings.warn('`ManyRelatedField()` is deprecated. ' - 'Use `RelatedField(many=True)` instead.', - DeprecationWarning, stacklevel=2) - kwargs['many'] = True - super(ManyRelatedField, self).__init__(*args, **kwargs) + 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) -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) + def to_internal_value(self, data): + try: + 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') + + def to_representation(self, obj): + return getattr(obj, self.slug_field) -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) +class ManyRelatedField(Field): + """ + Relationships with `many=True` transparently get coerced into instead being + a ManyRelatedField with a child relationship. + The `ManyRelatedField` class is responsible for handling iterating through + the values and passing each one to the child 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) + 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) + + 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 + ] + + 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 + + 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 3a03ca33..920d2bc4 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,170 +40,77 @@ 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. - # See: http://www.ietf.org/rfc/rfc4627.txt - - def render(self, data, accepted_media_type=None, renderer_context=None): - """ - Render `data` into JSON. - """ - if data is None: - return '' + ensure_ascii = not api_settings.UNICODE_JSON + compact = api_settings.COMPACT_JSON - # 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) + # 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 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 - - ret = json.dumps(data, cls=self.encoder_class, - indent=indent, ensure_ascii=self.ensure_ascii) - - # 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)) - 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');' - + return zero_as_none(max(min(int(params['indent']), 8), 0)) + except (KeyError, ValueError, TypeError): + pass -class XMLRenderer(BaseRenderer): - """ - Renderer which serializes to XML. - """ - - media_type = 'application/xml' - format = 'xml' - charset = 'utf-8' + # 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): """ - Renders *obj* into serialized XML. + Render `data` into JSON, returning a bytestring. """ if data is None: - return '' - - stream = StringIO() - - xml = SimplerXMLGenerator(stream, self.charset) - xml.startDocument() - xml.startElement("root", {}) - - self._to_xml(xml, data) + return bytes() - 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 + 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: - xml.characters(smart_text(data)) + separators = INDENT_SEPARATORS + ret = json.dumps( + data, cls=self.encoder_class, + indent=indent, ensure_ascii=self.ensure_ascii, + separators=separators + ) -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) + # 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): + # 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 TemplateHTMLRenderer(BaseRenderer): @@ -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 - - kwargs = {} - kwargs['required'] = v.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(v, 'choices', None) is not None: - kwargs['choices'] = v.choices - - if getattr(v, 'regex', None) is not None: - kwargs['regex'] = v.regex - - if getattr(v, 'widget', None): - widget = copy.deepcopy(v.widget) - kwargs['widget'] = widget - - if getattr(v, 'default', None) is not None: - kwargs['initial'] = v.default - - if getattr(v, 'label', None) is not None: - kwargs['label'] = v.label - - 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): + def get_rendered_html_form(self, data, 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 + Return a string representing a rendered HTML form, possibly bound to + either the input or output data. - 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) + 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 - # 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 + # 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 = {} + existing_serializer = None + + with override_method(view, request, method) as request: + if not self.show_form_for_method(view, method, request, instance): + 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 any(is_form_media_type(parser.media_type) for parser in view.parser_classes) + ): + return + + 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 hasattr(serializer, 'initial_data'): + serializer.is_valid() + + 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')] + ) + ) - 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) """ - - # 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 - - 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] - - # 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 - ) - - return GenericContentForm() + # 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 + + 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 + if not self.show_form_for_method(view, method, request, instance): + return + + # 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. + + if method in ('PUT', 'PATCH'): + serializer = view.get_serializer(instance=instance) + else: + serializer = view.get_serializer() + + # 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 + + # 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, + '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 919716f4..e4b5bc26 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,13 +198,31 @@ 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): """ Parses the request body and returns the data. @@ -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 5877c8a3..c21c60a2 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 a51b07f5..a251d99d 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -1,13 +1,26 @@ """ -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 930011d3..b1e39ff7 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 ce11b213..00000000 --- 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 da36d23f..00000000 --- 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 b3702d0b..00000000 --- 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 ed5baeae..00000000 --- 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 682a99a4..2eef6eeb 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): - """ - The default ValidationError behavior is to stringify each item in the list - if the messages are a list of error messages. +# 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' +) + - 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. +# BaseSerializer +# -------------- - We need to override the default behavior to get properly nested error dicts. +class BaseSerializer(Field): """ + The BaseSerializer class provides a minimal class which may be used + for writing custom serializer implementations. - def __init__(self, message): - if isinstance(message, dict): - self.messages = [message] - else: - self.messages = message + Note that we strongly restrict the ordering of operations/properties + that may be used on the serializer in order to enforce correct usage. + In particular, if a `data=` argument is passed then: -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) + .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: -class SortedDictWithMetadata(SortedDict): + .is_valid() - Not available. + .initial_data - Not available. + .validated_data - Not available. + .errors - Not available. + .data - Available. """ - A sorted dict-like object, that can have additional properties attached. - """ - def __getstate__(self): + + 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) + + 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) + + @classmethod + def many_init(cls, *args, **kwargs): """ - 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. + 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 SortedDict(self).__dict__ + 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 to_internal_value(self, data): + raise NotImplementedError('`to_internal_value()` must be implemented.') -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 to_representation(self, instance): + raise NotImplementedError('`to_representation()` must be implemented.') + def update(self, instance, validated_data): + raise NotImplementedError('`update()` must be implemented.') -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'). + def create(self, validated_data): + raise NotImplementedError('`create()` must be implemented.') - 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) + 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__) + ) - # 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 + assert hasattr(self, '_errors'), ( + 'You must call `.is_valid()` before calling `.save()`.' + ) - return SortedDict(fields) + assert not self.errors, ( + 'You cannot call `.save()` on a serializer with invalid data.' + ) + validated_data = dict( + list(self.validated_data.items()) + + list(kwargs.items()) + ) -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) + 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.instance = self.create(validated_data) + assert self.instance is not None, ( + '`create()` did not return an object instance.' + ) + return self.instance -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', ()) + 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__) + ) + assert hasattr(self, 'initial_data'), ( + 'Cannot call `.is_valid()` as no `data=` keyword argument was ' + 'passed when instantiating the serializer instance.' + ) -class BaseSerializer(WritableField): - """ - This is the Serializer implementation. - We need to implement it as `BaseSerializer` due to metaclass magicks. - """ - class Meta(object): - pass + 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: + self._errors = {} - _options_class = SerializerOptions - _dict_class = SortedDictWithMetadata + if self._errors and raise_exception: + raise ValidationError(self._errors) - def __init__(self, instance=None, data=None, files=None, - context=None, partial=False, many=None, - allow_add_remove=False, **kwargs): - 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 + return not bool(self._errors) - self.context = context or {} + @property + def data(self): + 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) - self.init_data = data - self.init_files = files - self.object = instance - self.fields = self.get_fields() + 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: + self._data = self.get_initial() + return self._data - self._data = None - self._files = None - self._errors = None - self._deleted = None + @property + def errors(self): + if not hasattr(self, '_errors'): + msg = 'You must call `.is_valid()` before accessing `.errors`.' + raise AssertionError(msg) + return self._errors - 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') + @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 - 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. +# Serializer & ListSerializer classes +# ----------------------------------- - def get_default_fields(self): - """ - Return the complete set of default fields for the object, as a dict. - """ - return {} +class SerializerMetaclass(type): + """ + This metaclass sets a dictionary named `_declared_fields` on the class. - def get_fields(self): - """ - Returns the complete set of fields for the object as a dict. + 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. + """ - This will be the set of any explicitly declared fields, - plus the set of fields returned by get_default_fields(). - """ - ret = SortedDict() + @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) - # Get the explicitly declared fields - base_fields = copy.deepcopy(self.base_fields) - for key, field in base_fields.items(): - ret[key] = field + # 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 - # 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 + return OrderedDict(fields) - # 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 __new__(cls, name, bases, attrs): + attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs) + return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) - # 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) - for key, field in ret.items(): - field.initialize(parent=self, field_name=key) +def get_validation_error_detail(exc): + assert isinstance(exc, (ValidationError, DjangoValidationError)) - return ret + 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] + } - ##### - # Methods to convert or revert from objects <--> primitive representations. - def get_field_key(self, field_name): +@six.add_metaclass(SerializerMetaclass) +class Serializer(BaseSerializer): + default_error_messages = { + 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.') + } + + @property + def fields(self): """ - Return the key that should be used for a given field. + A dictionary of {field_name: field_instance}. """ - return field_name + # `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 restore_fields(self, data, files): + def get_fields(self): """ - Core of deserialization, together with `restore_object`. - Converts a dictionary of data into a dictionary of deserialized fields. + Returns a dictionary of {field_name: field_instance}. """ - reverted_data = {} + # 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) - if data is not None and not isinstance(data, dict): - self._errors['non_field_errors'] = ['Invalid data'] - return None + 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 - 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) + 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 reverted_data + return value - def perform_validation(self, attrs): + def to_internal_value(self, data): """ - Run `validate_<fieldname>()` and `validate()` methods on the serializer + Dict of native values <- Dict of primitive datatypes. """ - 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: + 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: - 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 + 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) - return attrs + if errors: + raise ValidationError(errors) - def validate(self, attrs): - """ - Stub method, to be overridden in Serializer subclasses - """ - return attrs + return ret - def restore_object(self, attrs, instance=None): + def to_representation(self, instance): """ - Deserialize a dictionary of attributes into an object instance. - You should override this method to control how deserialized objects - are instantiated. + Object instance -> Dict of primitive datatypes. """ - if instance is not None: - instance.update(attrs) - return instance - return attrs + ret = OrderedDict() + fields = [field for field in self.fields.values() if not field.write_only] - def to_native(self, obj): - """ - Serialize objects -> primitives. - """ - ret = self._dict_class() - ret.fields = {} + 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) - 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) - else: - self._errors['non_field_errors'] = ['No input provided'] + def validate(self, attrs): + return attrs - if not self._errors: - return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def __repr__(self): + return unicode_to_repr(representation.serializer_repr(self, indent=1)) - 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) + # 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. - try: - source = self.source or field_name - value = obj + def __iter__(self): + for field in self.fields.values(): + yield self[field.field_name] - for component in source.split('.'): - value = get_component(value, component) - if value is None: - break - except ObjectDoesNotExist: - return None + 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) - if is_simple_callable(getattr(value, 'all', None)): - return [self.to_native(item) for item in value.all()] + # Include a backlink to the serializer class on return objects. + # Allows renderers such as HTMLFormRenderer to get the full field info. - if value is None: - return None + @property + def data(self): + ret = super(Serializer, self).data + return ReturnDict(ret, serializer=self) - if self.many is not None: - many = self.many - else: - many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type)) + @property + def errors(self): + ret = super(Serializer, self).errors + return ReturnDict(ret, serializer=self) - 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 +# There's some replication of `ListField` here, +# but that's probably better than obfuscating the call hierarchy. - 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) - else: - if self.required: - raise ValidationError(self.error_messages['required']) - return +class ListSerializer(BaseSerializer): + child = None + many = True - # Set the serializer object if it exists - obj = getattr(self.parent.object, field_name) if self.parent.object else None + default_error_messages = { + 'not_a_list': _('Expected a list of items but got type "{input_type}".') + } - 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 __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_identity(self, data): + def get_value(self, dictionary): """ - This hook is required for bulk update. - It is used to determine the canonical identity of a given object. + 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) - 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. + 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: - return data.get('id', None) - except AttributeError: - return None + 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)) - @property - def errors(self): + return value + + def to_internal_value(self, data): """ - Run deserialization and return error data, - setting self.object if no errors occurred. + List of dicts of native values <- List of dicts of primitive datatypes. """ - if self._errors is None: - data, files = self.init_data, self.init_files + if html.is_html_input(data): + data = html.parse_html_list(data) - 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 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] + }) - if not self._errors: - self.object = ret + ret = [] + errors = [] - return self._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({}) - def is_valid(self): - return not self.errors + if any(errors): + raise ValidationError(errors) - @property - def data(self): + return ret + + def to_representation(self, data): """ - Returns the serialized data on the serializer. + List of object instances -> List of dicts of primitive datatypes. """ - if self._data is None: - obj = self.object + # 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 + ] - if self.many is not None: - many = self.many - 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) - - return self._data + def validate(self, attrs): + return attrs - def save_object(self, obj, **kwargs): - obj.save(**kwargs) + 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 delete_object(self, obj): - obj.delete() + 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) -class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)): - pass +# ModelSerializer & HyperlinkedModelSerializer +# -------------------------------------------- -class ModelSerializerOptions(SerializerOptions): +def raise_errors_on_nested_writes(method_name, serializer, validated_data): """ - Meta class options for ModelSerializer + 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. """ - 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 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__ + ) + ) + + # 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: + + * 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. - field_mapping = { + 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.BigIntegerField: IntegerField, + models.BooleanField: BooleanField, + models.CharField: CharField, + models.CommaSeparatedIntegerField: CharField, + models.DateField: DateField, + models.DateTimeField: DateTimeField, + models.DecimalField: DecimalField, + models.EmailField: EmailField, + models.Field: ModelField, + models.FileField: FileField, models.FloatField: FloatField, + models.ImageField: ImageField, models.IntegerField: IntegerField, + models.NullBooleanField: NullBooleanField, models.PositiveIntegerField: IntegerField, - models.SmallIntegerField: IntegerField, models.PositiveSmallIntegerField: IntegerField, - models.DateTimeField: DateTimeField, - models.DateField: DateField, - models.TimeField: TimeField, - models.DecimalField: DecimalField, - models.EmailField: EmailField, - models.CharField: CharField, - models.URLField: URLField, models.SlugField: SlugField, + models.SmallIntegerField: IntegerField, models.TextField: CharField, - models.CommaSeparatedIntegerField: CharField, - models.BooleanField: BooleanField, - models.FileField: FileField, - models.ImageField: ImageField, + models.TimeField: TimeField, + models.URLField: URLField, } + serializer_related_field = PrimaryKeyRelatedField + serializer_url_field = HyperlinkedIdentityField + serializer_choice_field = ChoiceField + + # Default `create` and `update` behavior... - def get_default_fields(self): + 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() - - for relation in reverse_rels: - accessor_name = relation.get_accessor_name() - if not self.opts.fields or accessor_name not in self.opts.fields: + declared_fields = copy.deepcopy(self._declared_fields) + model = getattr(self.Meta, 'model') + depth = getattr(self.Meta, 'depth', 0) + + 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) - else: - field = self.get_related_field(None, related_model, to_many) - - if field: - if has_through_model: - field.read_only = True - - ret[accessor_name] = field - - # 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 + # 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 + ) - return ret + # Create the serializer field. + fields[field_name] = field_class(**field_kwargs) - def get_pk_field(self, model_field): + # 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 a default instance of the pk field. + 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. """ - return self.get_field(model_field) + fields = getattr(self.Meta, 'fields', None) + exclude = getattr(self.Meta, 'exclude', None) - def get_nested_field(self, model_field, related_model, to_many): - """ - Creates a default instance of a nested relational field. + if fields and not isinstance(fields, (list, tuple)): + raise TypeError( + 'The `fields` option must be a list or tuple. Got %s.' % + type(fields).__name__ + ) - Note that model_field will be `None` for reverse relationships. - """ - class NestedModelSerializer(ModelSerializer): - class Meta: - model = related_model - depth = self.opts.depth - 1 + if exclude and not isinstance(exclude, (list, tuple)): + raise TypeError( + 'The `exclude` option must be a list or tuple. Got %s.' % + type(exclude).__name__ + ) - return NestedModelSerializer(many=to_many) + 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) - def get_related_field(self, model_field, related_model, to_many): + 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. """ - Creates a default instance of a flat relational field. + 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... - Note that model_field will be `None` for reverse relationships. + def build_field(self, field_name, info, model_class, nested_depth): """ - # TODO: filter queryset using: - # .using(db).complex_filter(self.rel.limit_choices_to) + 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: + return self.build_nested_field(field_name, relation_info, nested_depth) - kwargs = { - 'queryset': related_model._default_manager, - 'many': to_many - } + elif hasattr(model_class, field_name): + return self.build_property_field(field_name, model_class) - if model_field: - kwargs['required'] = not(model_field.null or model_field.blank) + elif field_name == api_settings.URL_FIELD_NAME: + return self.build_url_field(field_name, model_class) - return PrimaryKeyRelatedField(**kwargs) + return self.build_unknown_field(field_name, model_class) - def get_field(self, model_field): + def build_standard_field(self, field_name, model_field): """ - Creates a default instance of a basic non-relational field. + Create regular model fields. """ - kwargs = {} + field_mapping = ClassLookupDict(self.serializer_field_mapping) + + field_class = field_mapping[model_field] + field_kwargs = get_field_kwargs(field_name, model_field) + + 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) - 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 + def build_relational_field(self, field_name, relation_info): + """ + Create fields for forward and reverse relationships. + """ + field_class = self.serializer_related_field + field_kwargs = get_relation_kwargs(field_name, relation_info) - if model_field.has_default(): - kwargs['default'] = model_field.get_default() + # `view_name` is only valid for hyperlinked relationships. + if not issubclass(field_class, HyperlinkedRelatedField): + field_kwargs.pop('view_name', None) - if issubclass(model_field.__class__, models.TextField): - kwargs['widget'] = widgets.Textarea + return field_class, field_kwargs - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + 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 = relation_info.related_model + depth = nested_depth - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + field_class = NestedSerializer + field_kwargs = get_nested_relation_kwargs(relation_info) - # TODO: TypedChoiceField? - if model_field.flatchoices: # This ModelField contains choices - kwargs['choices'] = model_field.flatchoices - return ChoiceField(**kwargs) + return field_class, field_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 + def build_property_field(self, field_name, model_class): + """ + Create a read only field for model methods and properties. + """ + field_class = ReadOnlyField + field_kwargs = {} - 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'], - } + return field_class, field_kwargs - if model_field.__class__ in attribute_dict: - attributes = attribute_dict[model_field.__class__] - for attribute in attributes: - kwargs.update({attribute: getattr(model_field, attribute)}) + def build_url_field(self, field_name, model_class): + """ + Create a field representing the object's own URL. + """ + field_class = self.serializer_url_field + field_kwargs = get_url_kwargs(model_class) - try: - return self.field_mapping[model_field.__class__](**kwargs) - except KeyError: - return ModelField(model_field=model_field, **kwargs) + return field_class, 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. + Include any 'extra_kwargs' that have been included for this field, + possibly removing any incompatible existing keyword arguments. + """ + 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) + + if extra_kwargs.get('default') and kwargs.get('required') is False: + kwargs.pop('required') - 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. + kwargs.update(extra_kwargs) + + return kwargs + + # Methods for determining additional keyword arguments to apply... + + def get_extra_kwargs(self): """ - try: - instance.full_clean(exclude=self.get_validation_exclusions()) - except ValidationError as err: - self._errors = err.message_dict - return None - return instance + Return a dictionary mapping field names to a dictionary of + additional keyword arguments. + """ + extra_kwargs = getattr(self.Meta, 'extra_kwargs', {}) + + 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 + + # 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={<field_name>: {'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 + + 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 + + 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 - def restore_object(self, attrs, instance=None): + return extra_kwargs + + def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs): """ - Restore the model instance. + 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') """ - m2m_data = {} - related_data = {} - meta = self.opts.model._meta + model = getattr(self.Meta, 'model') + model_fields = self._get_model_fields( + field_names, declared_fields, 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) + # 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 - # 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) + 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 - # Forward m2m relations - for field in meta.many_to_many: - if field.name in attrs: - m2m_data[field.name] = attrs.pop(field.name) + return extra_kwargs, hidden_fields - # Update an existing instance... - if instance is not None: - for key, val in attrs.items(): - setattr(instance, key, val) + def _get_model_fields(self, field_names, declared_fields, extra_kwargs): + """ + 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`. + """ + model = getattr(self.Meta, 'model') + model_fields = {} + + 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 '.' in source or source == '*': + # Model fields will always have a simple source mapping, + # they can't be nested attribute lookups. + continue - # ...or create a new instance - else: - instance = self.opts.model(**attrs) + try: + field = model._meta.get_field(source) + if isinstance(field, DjangoModelField): + model_fields[source] = field + except FieldDoesNotExist: + pass - # 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 model_fields - return instance + # Determine the validators to apply... - def from_native(self, data, files): + def get_validators(self): """ - Override the default method to also include model field validation. + Determine the set of validators to use when instantiating serializer. """ - instance = super(ModelSerializer, self).from_native(data, files) - if not self._errors: - return self.full_clean(instance) + # 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 save_object(self, obj, **kwargs): + def get_unique_together_validators(self): """ - Save the deserialized object and return it. + Determine a default set of validators for any unique_together contraints. """ - obj.save(**kwargs) + model_class_inheritance_tree = ( + [self.Meta.model] + + list(self.Meta.model._meta.parents.keys()) + ) - 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) + # 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 - if getattr(obj, '_related_data', None): - for accessor_name, related in obj._related_data.items(): - setattr(obj, accessor_name, related) - del(obj._related_data) + 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()] -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) + 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) -class HyperlinkedModelSerializer(ModelSerializer): - """ - A subclass of ModelSerializer that uses hyperlinked relationships, - instead of primary key relationships. - """ - _options_class = HyperlinkedModelSerializerOptions - _default_view_name = '%(model_name)s-detail' - _hyperlink_field_class = HyperlinkedRelatedField + 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) - def get_default_fields(self): - fields = super(HyperlinkedModelSerializer, self).get_default_fields() + 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) - if self.opts.view_name is None: - self.opts.view_name = self._get_default_view_name(self.opts.model) + return validators - 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 +if hasattr(models, 'UUIDField'): + ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField - 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) +if postgres_fields: + class CharMappingField(DictField): + child = CharField() - def get_related_field(self, model_field, related_model, to_many): - """ - Creates a default instance of a flat relational field. - """ - # 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 - } + ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField + ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField - 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 +class HyperlinkedModelSerializer(ModelSerializer): + """ + A type of `ModelSerializer` that uses hyperlinked relationships instead + of primary key relationships. Specifically: - return self._hyperlink_field_class(**kwargs) + * A 'url' field is included instead of the 'id' field. + * Relationships to other instances are hyperlinks, instead of primary keys. + """ + serializer_related_field = HyperlinkedRelatedField - def get_identity(self, data): + def get_default_field_names(self, declared_fields, model_info): """ - This hook is required for bulk update. - We need to override the default, to use the url as the identity. + Return the default list of field names that will be used if the + `Meta.fields` option is not specified. """ - try: - return data.get('url', None) - except AttributeError: - return None + return ( + [api_settings.URL_FIELD_NAME] + + list(declared_fields.keys()) + + list(model_info.fields.keys()) + + list(model_info.forward_relations.keys()) + ) - def _get_default_view_name(self, model): + def build_nested_field(self, field_name, relation_info, nested_depth): """ - Return the view name to use if 'view_name' is not specified in 'Meta' + Create nested fields for forward and reverse relationships. """ - 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 + class NestedSerializer(HyperlinkedModelSerializer): + class Meta: + model = relation_info.related_model + depth = nested_depth - 1 + + 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 8fd177d5..a3e9f590 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_CONTENT_NEGOTIATION_CLASS': - 'rest_framework.negotiation.DefaultContentNegotiation', + 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', + 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata', + 'DEFAULT_VERSIONING_CLASS': None, - # 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 9e382312..00000000 --- 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 <benjamin@python.org>" -__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 6bfb778c..04f12ed3 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 .dropdown-menu li a, .navbar .dropdown-menu li { + color: #A30000; } -.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ - color: #A30000; +.navbar .dropdown-menu li a:hover { + background: #EEEEEE; + color: #C20000; } -.navbar .navbar-inner .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; -} - -.tabbable.first-tab-active .tab-content -{ - border-top-right-radius: 0; + background: #F5F5F5; } -#footer, #push { - height: 60px; /* .push must be the same height as .footer */ +.tabbable.first-tab-active .tab-content { + border-top-right-radius: 0; } -#footer{ - text-align: right; +footer { + position: absolute; + bottom: 0; + left: 0; + clear: both; + z-index: 10; + height: 60px; + width: 95%; + margin: 0 2.5%; } -#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 373f4b43..a9f35cee 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 0261a303..4f52cc56 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 Binary files differnew file mode 100644 index 00000000..4a4ca865 --- /dev/null +++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot 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 00000000..25691af8 --- /dev/null +++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg @@ -0,0 +1,229 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" > +<svg xmlns="http://www.w3.org/2000/svg"> +<metadata></metadata> +<defs> +<font id="glyphicons_halflingsregular" horiz-adv-x="1200" > +<font-face units-per-em="1200" ascent="960" descent="-240" /> +<missing-glyph horiz-adv-x="500" /> +<glyph /> +<glyph /> +<glyph unicode="
" /> +<glyph unicode=" " /> +<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" /> +<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" /> +<glyph unicode=" " /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="652" /> +<glyph unicode=" " horiz-adv-x="1304" /> +<glyph unicode=" " horiz-adv-x="434" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="217" /> +<glyph unicode=" " horiz-adv-x="163" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="72" /> +<glyph unicode=" " horiz-adv-x="260" /> +<glyph unicode=" " horiz-adv-x="326" /> +<glyph unicode="€" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" /> +<glyph unicode="−" d="M200 400h900v300h-900v-300z" /> +<glyph unicode="◼" horiz-adv-x="500" d="M0 0z" /> +<glyph unicode="☁" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" /> +<glyph unicode="✉" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" /> +<glyph unicode="✏" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" /> +<glyph unicode="" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" /> +<glyph unicode="" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" /> +<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" /> +<glyph unicode="" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" /> +<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" /> +<glyph unicode="" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" /> +<glyph unicode="" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" /> +<glyph unicode="" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" /> +<glyph unicode="" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" /> +<glyph unicode="" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" /> +<glyph unicode="" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" /> +<glyph unicode="" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" /> +<glyph unicode="" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" /> +<glyph unicode="" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" /> +<glyph unicode="" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" /> +<glyph unicode="" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" /> +<glyph unicode="" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" /> +<glyph unicode="" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" /> +<glyph unicode="" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" /> +<glyph unicode="" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" /> +<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" /> +<glyph unicode="" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" /> +<glyph unicode="" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" /> +<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" /> +<glyph unicode="" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" /> +<glyph unicode="" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" /> +<glyph unicode="" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" /> +<glyph unicode="" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" /> +<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" /> +<glyph unicode="" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" /> +<glyph unicode="" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" /> +<glyph unicode="" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" /> +<glyph unicode="" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" /> +<glyph unicode="" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" /> +<glyph unicode="" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" /> +<glyph unicode="" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" /> +<glyph unicode="" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" /> +<glyph unicode="" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" /> +<glyph unicode="" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" /> +<glyph unicode="" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" /> +<glyph unicode="" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" /> +<glyph unicode="" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" /> +<glyph unicode="" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " /> +<glyph unicode="" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" /> +<glyph unicode="" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" /> +<glyph unicode="" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" /> +<glyph unicode="" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " /> +<glyph unicode="" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" /> +<glyph unicode="" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" /> +<glyph unicode="" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" /> +<glyph unicode="" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" /> +<glyph unicode="" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" /> +<glyph unicode="" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" /> +<glyph unicode="" d="M200 0l900 550l-900 550v-1100z" /> +<glyph unicode="" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" /> +<glyph unicode="" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" /> +<glyph unicode="" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" /> +<glyph unicode="" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" /> +<glyph unicode="" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" /> +<glyph unicode="" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" /> +<glyph unicode="" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" /> +<glyph unicode="" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" /> +<glyph unicode="" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" /> +<glyph unicode="" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" /> +<glyph unicode="" d="M0 547l600 453v-300h600v-300h-600v-301z" /> +<glyph unicode="" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" /> +<glyph unicode="" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" /> +<glyph unicode="" d="M104 600h296v600h300v-600h298l-449 -600z" /> +<glyph unicode="" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" /> +<glyph unicode="" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" /> +<glyph unicode="" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" /> +<glyph unicode="" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" /> +<glyph unicode="" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" /> +<glyph unicode="" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" /> +<glyph unicode="" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" /> +<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" /> +<glyph unicode="" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" /> +<glyph unicode="" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" /> +<glyph unicode="" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" /> +<glyph unicode="" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" /> +<glyph unicode="" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" /> +<glyph unicode="" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" /> +<glyph unicode="" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" /> +<glyph unicode="" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" /> +<glyph unicode="" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" /> +<glyph unicode="" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" /> +<glyph unicode="" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" /> +<glyph unicode="" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" /> +<glyph unicode="" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" /> +<glyph unicode="" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" /> +<glyph unicode="" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" /> +<glyph unicode="" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" /> +<glyph unicode="" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" /> +<glyph unicode="" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" /> +<glyph unicode="" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" /> +<glyph unicode="" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" /> +<glyph unicode="" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" /> +<glyph unicode="" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" /> +<glyph unicode="" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" /> +<glyph unicode="" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" /> +<glyph unicode="" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" /> +<glyph unicode="" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" /> +<glyph unicode="" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" /> +<glyph unicode="" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" /> +<glyph unicode="" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" /> +<glyph unicode="" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" /> +<glyph unicode="" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" /> +<glyph unicode="" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" /> +<glyph unicode="" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" /> +<glyph unicode="" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" /> +<glyph unicode="" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" /> +<glyph unicode="" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" /> +<glyph unicode="" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" /> +<glyph unicode="" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" /> +<glyph unicode="" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" /> +<glyph unicode="" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" /> +<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" /> +<glyph unicode="" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" /> +<glyph unicode="" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" /> +<glyph unicode="" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" /> +<glyph unicode="" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" /> +<glyph unicode="" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" /> +<glyph unicode="" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" /> +<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " /> +<glyph unicode="" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" /> +<glyph unicode="" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" /> +<glyph unicode="" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" /> +<glyph unicode="" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" /> +<glyph unicode="" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" /> +<glyph unicode="" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" /> +<glyph unicode="" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" /> +<glyph unicode="" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" /> +<glyph unicode="" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" /> +<glyph unicode="" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" /> +<glyph unicode="" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" /> +<glyph unicode="" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" /> +<glyph unicode="" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" /> +<glyph unicode="" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" /> +<glyph unicode="" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" /> +<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" /> +<glyph unicode="" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" /> +<glyph unicode="" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" /> +<glyph unicode="" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" /> +</font> +</defs></svg>
\ 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 Binary files differnew file mode 100644 index 00000000..67fa00bf --- /dev/null +++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff Binary files differnew file mode 100644 index 00000000..8c54182a --- /dev/null +++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff diff --git a/rest_framework/static/rest_framework/js/bootstrap.min.js b/rest_framework/static/rest_framework/js/bootstrap.min.js index e0b220f4..7c1561a8 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('<div class="modal-backdrop '+d+'" />').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<d.length-1&&i++,~i||(i=0),d.eq(i).focus()}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api touchstart.dropdown.data-api",d),a("body").on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",b,c.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",b+", [role=menu]",c.prototype.keydown)})}(window.jQuery),!function(a){function b(b,c){var d=a.proxy(this.process,this),e=a(b).is("body")?a(window):a(b),f;this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=e.on("scroll.scroll-spy.data-api",d),this.selector=(this.options.target||(f=a(b).attr("href"))&&f.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > 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:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',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:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(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<this.options.minLength?this.shown?this.hide():this:(c=a.isFunction(this.source)?this.source(this.query,a.proxy(this.process,this)):this.source,c?this.process(c):this)},process:function(b){var c=this;return b=a.grep(b,function(a){return c.matcher(a)}),b=this.sorter(b),b.length?this.render(b.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){var b=[],c=[],d=[],e;while(e=a.shift())e.toLowerCase().indexOf(this.query.toLowerCase())?~e.indexOf(this.query)?c.push(e):d.push(e):b.push(e);return b.concat(c,d)},highlighter:function(a){var b=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return a.replace(new RegExp("("+b+")","ig"),function(a,b){return"<strong>"+b+"</strong>"})},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:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',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('<div class="dropdown-backdrop"/>').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<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f+', [role="menu"], [role="listbox"]',g.prototype.keydown)}(jQuery),+function(a){"use strict";function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$backdrop=this.isShown=null,this.scrollbarWidth=0,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.2.0",c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.$body.addClass("modal-open"),this.setScrollbar(),this.escape(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(c.$body),c.$element.show().scrollTop(0),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one("bsTransitionEnd",function(){c.$element.trigger("focus").trigger(e)}).emulateTransitionEnd(300):c.$element.trigger("focus").trigger(e)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.$body.removeClass("modal-open"),this.resetScrollbar(),this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;if(this.$backdrop=a('<div class="modal-backdrop '+d+'" />').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:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',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-k<o.left?"right":g,e.removeClass(m).addClass(g)}var p=this.getCalculatedOffset(g,j,k,l);this.applyPlacement(p,g);var q=function(){d.$element.trigger("shown.bs."+d.type),d.hoverState=null};a.support.transition&&this.$tip.hasClass("fade")?e.one("bsTransitionEnd",q).emulateTransitionEnd(150):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top=b.top+g,b.left=b.left+h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=k.left?2*k.left-e+i:2*k.top-f+j,m=k.left?"left":"top",n=k.left?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(l,d[0][n],m)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},c.prototype.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")},c.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach(),c.$element.trigger("hidden.bs."+c.type)}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.removeAttr("aria-describedby"),this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one("bsTransitionEnd",b).emulateTransitionEnd(150):b(),this.hoverState=null,this)},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName;return a.extend({},"function"==typeof c.getBoundingClientRect?c.getBoundingClientRect():null,{scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop(),width:d?a(window).width():b.outerWidth(),height:d?a(window).height():b.outerHeight()},d?{top:0,left:0}:b.offset())},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.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:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),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 c74829d7..c8812132 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 b9f249f9..90a75508 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 00000000..96f924ed --- /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 %} +<!-- form.non_field_errors --> diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 9d939e73..877387f2 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 %} <!DOCTYPE html> <html> <head> - {% block head %} + {% block head %} - {% block meta %} - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <meta name="robots" content="NONE,NOARCHIVE" /> - {% endblock %} + {% block meta %} + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> + <meta name="robots" content="NONE,NOARCHIVE" /> + {% endblock %} - <title>{% block title %}Django REST framework{% endblock %}</title> + <title>{% block title %}Django REST framework{% endblock %}</title> - {% block style %} - {% block bootstrap_theme %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> - {% endblock %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/> - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> - {% endblock %} + {% block style %} + {% block bootstrap_theme %} + <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> + <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> + {% endblock %} + <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/> + <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> + {% endblock %} - {% endblock %} + {% endblock %} </head> - <body class="{% block bodyclass %}{% endblock %} container"> - - <div class="wrapper"> - - {% block navbar %} - <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> - <div class="navbar-inner"> - <div class="container-fluid"> - <span href="/"> - {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} - </span> - <ul class="nav pull-right"> - {% block userlinks %} - {% if user.is_authenticated %} - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"> - {{ user }} - <b class="caret"></b> + {% block body %} + <body class="{% block bodyclass %}{% endblock %}"> + + <div class="wrapper"> + + {% block navbar %} + <div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> + <div class="container"> + <span> + {% block branding %} + <a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'> + Django REST framework <span class="version">{{ version }}</span> </a> - <ul class="dropdown-menu"> - <li>{% optional_logout request %}</li> - </ul> - </li> + {% endblock %} + </span> + <ul class="nav navbar-nav pull-right"> + {% block userlinks %} + {% if user.is_authenticated %} + {% optional_logout request user %} + {% else %} + {% optional_login request %} + {% endif %} + {% endblock %} + </ul> + </div> + </div> + {% endblock %} + + <div class="container"> + {% block breadcrumbs %} + <ul class="breadcrumb"> + {% for breadcrumb_name, breadcrumb_url in breadcrumblist %} + {% if forloop.last %} + <li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li> {% else %} - <li>{% optional_login request %}</li> + <li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li> {% endif %} - {% endblock %} + {% endfor %} </ul> - </div> - </div> - </div> - {% endblock %} + {% endblock %} - {% block breadcrumbs %} - <ul class="breadcrumb"> - {% for breadcrumb_name, breadcrumb_url in breadcrumblist %} - <li> - <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">›</span>{% endif %} - </li> - {% endfor %} - </ul> - {% endblock %} + <!-- Content --> + <div id="content"> - <!-- Content --> - <div id="content"> - - {% if 'GET' in allowed_methods %} - <form id="get-form" class="pull-right"> - <fieldset> - <div class="btn-group format-selection"> - <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a> - - <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {% for format in available_formats %} - <li> - <a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a> - </li> - {% endfor %} - </ul> - </div> + {% if 'GET' in allowed_methods %} + <form id="get-form" class="pull-right"> + <fieldset> + <div class="btn-group format-selection"> + <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' + rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a> - </fieldset> - </form> - {% endif %} - - {% if options_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> - {% csrf_token %} - <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> - <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> - </form> - {% endif %} - - {% if delete_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> - {% csrf_token %} - <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> - <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> - </form> - {% endif %} - - <div class="content-main"> - <div class="page-header"><h1>{{ name }}</h1></div> - {{ description }} - <div class="request-info" style="clear: both" > - <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> - </div> - <div class="response-info"> - <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} -{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span> -{% endfor %} -</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} - </div> - </div> + <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" + title="Specify a format for the GET request"> + <span class="caret"></span> + </button> + <ul class="dropdown-menu"> + {% for format in available_formats %} + <li> + <a class="js-tooltip format-option" + href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' + rel="nofollow" + title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`"> + {{ format }} + </a> + </li> + {% endfor %} + </ul> + </div> + </fieldset> + </form> + {% endif %} + + {% if options_form %} + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> + {% csrf_token %} + <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> + <button class="btn btn-primary js-tooltip" + title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> + </form> + {% endif %} - {% if response.status_code != 403 %} + {% if delete_form %} + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> + {% csrf_token %} + <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> + <button class="btn btn-danger js-tooltip" + title="Make a DELETE request on the {{ name }} resource">DELETE</button> + </form> + {% endif %} + + <div class="content-main"> + <div class="page-header"> + <h1>{{ name }}</h1> + </div> + <div style="float:left"> + {% block description %} + {{ description }} + {% endblock %} + </div> - {% if post_form or raw_data_post_form %} - <div {% if post_form %}class="tabbable"{% endif %}> - {% if post_form %} - <ul class="nav nav-tabs form-switcher"> - <li><a href="#object-form" data-toggle="tab">HTML form</a></li> - <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li> - </ul> + {% if paginator %} + <nav style="float: right"> + {% get_pagination_html paginator %} + </nav> {% endif %} - <div class="well tab-content"> - {% if post_form %} - <div class="tab-pane" id="object-form"> - {% with form=post_form %} - <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> - <fieldset> - {% include "rest_framework/form.html" %} - <div class="form-actions"> - <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> - </div> - </fieldset> - </form> - {% endwith %} - </div> - {% endif %} - <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form"> - {% with form=raw_data_post_form %} - <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> - <fieldset> - {% include "rest_framework/form.html" %} - <div class="form-actions"> - <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> - </div> - </fieldset> - </form> - {% endwith %} - </div> + + <div class="request-info" style="clear: both" > + <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> + </div> + <div class="response-info"> + <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} +{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span> +{% endfor %} +</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} </div> </div> - {% endif %} - {% if put_form or raw_data_put_form or raw_data_patch_form %} - <div {% if put_form %}class="tabbable"{% endif %}> - {% if put_form %} - <ul class="nav nav-tabs form-switcher"> - <li><a href="#object-form" data-toggle="tab">HTML form</a></li> - <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li> - </ul> - {% endif %} - <div class="well tab-content"> - {% if put_form %} - <div class="tab-pane" id="object-form"> - {% with form=put_form %} - <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal"> - <fieldset> - {% include "rest_framework/form.html" %} - <div class="form-actions"> - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + {% if display_edit_forms %} + + {% if post_form or raw_data_post_form %} + <div {% if post_form %}class="tabbable"{% endif %}> + {% if post_form %} + <ul class="nav nav-tabs form-switcher"> + <li> + <a name='html-tab' href="#post-object-form" data-toggle="tab">HTML form</a> + </li> + <li> + <a name='raw-tab' href="#post-generic-content-form" data-toggle="tab">Raw data</a> + </li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if post_form %} + <div class="tab-pane" id="post-object-form"> + {% with form=post_form %} + <form action="{{ request.get_full_path }}" + method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> + <fieldset> + {{ post_form }} + <div class="form-actions"> + <button class="btn btn-primary" + title="Make a POST request on the {{ name }} resource">POST</button> + </div> + </fieldset> + </form> + {% endwith %} </div> - </fieldset> - </form> - {% endwith %} + {% endif %} + <div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form"> + {% with form=raw_data_post_form %} + <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> + <fieldset> + {% include "rest_framework/raw_data_form.html" %} + <div class="form-actions"> + <button class="btn btn-primary" + title="Make a POST request on the {{ name }} resource">POST</button> + </div> + </fieldset> + </form> + {% endwith %} + </div> + </div> </div> - {% endif %} - <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form"> - {% with form=raw_data_put_or_patch_form %} - <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> - <fieldset> - {% include "rest_framework/form.html" %} - <div class="form-actions"> - {% if raw_data_put_form %} - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> - {% endif %} - {% if raw_data_patch_form %} - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PUT request on the {{ name }} resource">PATCH</button> - {% endif %} + {% endif %} + + {% if put_form or raw_data_put_form or raw_data_patch_form %} + <div {% if put_form %}class="tabbable"{% endif %}> + {% if put_form %} + <ul class="nav nav-tabs form-switcher"> + <li> + <a name='html-tab' href="#put-object-form" data-toggle="tab">HTML form</a> + </li> + <li> + <a name='raw-tab' href="#put-generic-content-form" data-toggle="tab">Raw data</a> + </li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if put_form %} + <div class="tab-pane" id="put-object-form"> + <form action="{{ request.get_full_path }}" + method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> + <fieldset> + {{ put_form }} + <div class="form-actions"> + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + </div> + </fieldset> + </form> </div> - </fieldset> - </form> - {% endwith %} + {% endif %} + <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form"> + {% with form=raw_data_put_or_patch_form %} + <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> + <fieldset> + {% include "rest_framework/raw_data_form.html" %} + <div class="form-actions"> + {% if raw_data_put_form %} + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + {% endif %} + {% if raw_data_patch_form %} + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button> + {% endif %} + </div> + </fieldset> + </form> + {% endwith %} + </div> + </div> </div> - </div> - </div> + {% endif %} {% endif %} - {% endif %} - - </div> - <!-- END content-main --> - - </div> - <!-- END Content --> - - <div id="push"></div> - - </div> - - </div><!-- ./wrapper --> - - {% block footer %} - <!--<div id="footer"> - <a class="powered-by" href='http://django-rest-framework.org'>Django REST framework</a> - </div>--> - {% endblock %} + </div> + <!-- END Content --> + </div><!-- /.container --> + </div><!-- ./wrapper --> - {% block script %} - <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> - <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> - <script src="{% static "rest_framework/js/prettify-min.js" %}"></script> - <script src="{% static "rest_framework/js/default.js" %}"></script> + {% block script %} + <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> + <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> + <script src="{% static "rest_framework/js/prettify-min.js" %}"></script> + <script src="{% static "rest_framework/js/default.js" %}"></script> + {% endblock %} + </body> {% endblock %} - </body> </html> diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html deleted file mode 100644 index b27f652e..00000000 --- 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 %} - <div class="control-group"> <!--{% if field.errors %}error{% endif %}--> - {{ field.label_tag|add_class:"control-label" }} - <div class="controls"> - {{ field }} - <span class="help-block">{{ field.help_text }}</span> - <!--{{ field.errors|add_class:"help-block" }}--> - </div> - </div> -{% 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 00000000..07a7308f --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/checkbox.html @@ -0,0 +1,16 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}> + {% if field.label %}{{ field.label }}{% endif %} + </label> + </div> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..ec7d5935 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html @@ -0,0 +1,30 @@ +<div class="form-group"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + {% if style.inline %} + {% for key, text in field.choices.items %} + <label class="checkbox-inline"> + <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> + {{ text }} + </label> + {% endfor %} + {% else %} + {% for key, text in field.choices.items %} + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} + {% endif %} + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..ba3e3aba --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/fieldset.html @@ -0,0 +1,13 @@ +{% load rest_framework %} +<fieldset> + {% if field.label %} + <div class="form-group" style="border-bottom: 1px solid #e5e5e5"> + <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend> + </div> + {% endif %} + {% for nested_field in field %} + {% if not nested_field.read_only %} + {% render_field nested_field style=style %} + {% endif %} + {% endfor %} +</fieldset> 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 00000000..fd15b626 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/form.html @@ -0,0 +1,15 @@ +{% load rest_framework %} +<form class="form-horizontal" role="form" action="." method="POST" novalidate> + {% csrf_token %} + {% for field in form %} + {% if not field.read_only %} + {% render_field field style=style %} + {% endif %} + {% endfor %} + <!-- form.non_field_errors --> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <button type="submit" class="btn btn-default">Submit</button> + </div> + </div> +</form> 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 00000000..c41cd523 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/input.html @@ -0,0 +1,14 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..a9ff04a6 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html @@ -0,0 +1,16 @@ +{% load rest_framework %} +<fieldset> + {% if field.label %} + <div class="form-group" style="border-bottom: 1px solid #e5e5e5"> + <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend> + </div> + {% endif %} + <!-- + <ul> + {% for child in field.value %} + <li>TODO</li> + {% endfor %} + </ul> + --> + <p>Lists are not currently supported in HTML input.</p> +</fieldset> 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 00000000..52238bb1 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/radio.html @@ -0,0 +1,30 @@ +<div class="form-group"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + {% if style.inline %} + {% for key, text in field.choices.items %} + <label class="radio-inline"> + <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> + {{ text }} + </label> + {% endfor %} + {% else %} + {% for key, text in field.choices.items %} + <div class="radio"> + <label> + <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} + {% endif %} + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..8a7fca37 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/select.html @@ -0,0 +1,21 @@ +<div class="form-group"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + <select class="form-control" name="{{ field.name }}"> + {% if field.allow_null or field.allow_blank %} + <option value="" {% if not field.value %}selected{% endif %}>--------</option> + {% endif %} + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> + {% endfor %} + </select> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..0735f280 --- /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 %} + +<div class="form-group"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> + {% empty %} + <option>{{ no_items }}</option> + {% endfor %} + </select> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..ec107549 --- /dev/null +++ b/rest_framework/templates/rest_framework/horizontal/textarea.html @@ -0,0 +1,14 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label> + {% endif %} + <div class="col-sm-10"> + <textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} + </div> +</div> 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 00000000..71737f15 --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/checkbox.html @@ -0,0 +1,8 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}> + {% if field.label %}{{ field.label }}{% endif %} + </label> + </div> +</div> 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 00000000..09349686 --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html @@ -0,0 +1,13 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + {% for key, text in field.choices.items %} + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} +</div> 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 00000000..e49b42fd --- /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 00000000..6a0ea81d --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/form.html @@ -0,0 +1,11 @@ +{% load rest_framework %} +<form class="form-inline" role="form" action="." method="POST" novalidate> + {% csrf_token %} + {% for field in form %} + {% if not field.read_only %} + {% render_field field style=style %} + {% endif %} + {% endfor %} + <!-- form.non_field_errors --> + <button type="submit" class="btn btn-default">Submit</button> +</form> 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 00000000..de85ba48 --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/input.html @@ -0,0 +1,6 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}> +</div> 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 00000000..2ae56d7c --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/list_fieldset.html @@ -0,0 +1 @@ +<span>Lists are not currently supported in HTML input.</span> 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 00000000..1915f4f8 --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/radio.html @@ -0,0 +1,13 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + {% for key, text in field.choices.items %} + <div class="radio"> + <label> + <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} +</div> 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 00000000..6b30e4d6 --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/select.html @@ -0,0 +1,13 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + <select class="form-control" name="{{ field.name }}"> + {% if field.allow_null or field.allow_blank %} + <option value="" {% if not field.value %}selected{% endif %}>--------</option> + {% endif %} + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> + {% endfor %} + </select> +</div> 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 00000000..5a8b2494 --- /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 %} + +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> + {% empty %} + <option>{{ no_items }}</option> + {% endfor %} + </select> +</div> 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 00000000..0766a01c --- /dev/null +++ b/rest_framework/templates/rest_framework/inline/textarea.html @@ -0,0 +1,6 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label class="sr-only">{{ field.label }}</label> + {% endif %} + <input name="{{ field.name }}" type="text" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}> +</div> diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index be9a0072..8e6240a6 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 %} -<html> - - <head> - {% block style %} - {% block bootstrap_theme %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> - {% endblock %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> - {% endblock %} - </head> + {% block body %} <body class="container"> <div class="container-fluid" style="margin-top: 30px"> @@ -25,23 +17,46 @@ <div class="row-fluid"> <div> - <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post"> + <form action="{% url 'rest_framework:login' %}" role="form" method="post"> {% csrf_token %} - <div id="div_id_username" class="clearfix control-group"> - <div class="controls"> - <Label class="span4">Username:</label> - <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username"> + <div id="div_id_username" + class="clearfix control-group {% if form.username.errors %}error{% endif %}"> + <div class="form-group"> + <label for="id_username">Username:</label> + <input type="text" name="username" maxlength="100" + autocapitalize="off" + autocorrect="off" class="form-control textinput textInput" + id="id_username" required + {% if form.username.value %}value="{{ form.username.value }}"{% endif %}> + {% if form.username.errors %} + <p class="text-error"> + {{ form.username.errors|striptags }} + </p> + {% endif %} </div> </div> - <div id="div_id_password" class="clearfix control-group"> - <div class="controls"> - <Label class="span4">Password:</label> - <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password"> + <div id="div_id_password" + class="clearfix control-group {% if form.password.errors %}error{% endif %}"> + <div class="form-group"> + <label for="id_password">Password:</label> + <input type="password" name="password" maxlength="100" + autocapitalize="off" autocorrect="off" class="form-control textinput textInput" + id="id_password" required> + {% if form.password.errors %} + <p class="text-error"> + {{ form.password.errors|striptags }} + </p> + {% endif %} </div> </div> <input type="hidden" name="next" value="{{ next }}" /> + {% if form.non_field_errors %} + {% for error in form.non_field_errors %} + <div class="well well-small text-error" style="border: none">{{ error }}</div> + {% endfor %} + {% endif %} <div class="form-actions-no-box"> - <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit"> + <input type="submit" name="submit" value="Log in" class="btn btn-primary form-control" id="submit-id-submit"> </div> </form> </div> @@ -50,4 +65,4 @@ </div><!-- /.row-fluid --> </div><!-- /.container-fluid --> </body> -</html> + {% 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 00000000..04045810 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/numbers.html @@ -0,0 +1,27 @@ +<ul class="pagination" style="margin: 5px 0 10px 0"> + {% if previous_url %} + <li><a href="{{ previous_url }}" aria-label="Previous"><span aria-hidden="true">«</span></a></li> + {% else %} + <li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">«</span></a></li> + {% endif %} + + {% for page_link in page_links %} + {% if page_link.is_break %} + <li class="disabled"> + <a href="#"><span aria-hidden="true">…</span></a> + </li> + {% else %} + {% if page_link.is_active %} + <li class="active"><a href="{{ page_link.url }}">{{ page_link.number }}</a></li> + {% else %} + <li><a href="{{ page_link.url }}">{{ page_link.number }}</a></li> + {% endif %} + {% endif %} + {% endfor %} + + {% if next_url %} + <li><a href="{{ next_url }}" aria-label="Next"><span aria-hidden="true">»</span></a></li> + {% else %} + <li class="disabled"><a href="#" aria-label="Next"><span aria-hidden="true">»</span></a></li> + {% endif %} +</ul> 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 00000000..eacbfff4 --- /dev/null +++ b/rest_framework/templates/rest_framework/pagination/previous_and_next.html @@ -0,0 +1,12 @@ +<ul class="pager"> +{% if previous_url %} + <li class="previous"><a href="{{ previous_url }}">« Previous</a></li> +{% else %} + <li class="previous disabled"><a href="#">« Previous</a></li> +{% endif %} +{% if next_url %} + <li class="next"><a href="{{ next_url }}">Next »</a></li> +{% else %} + <li class="next disabled"><a href="#">Next »</li> +{% endif %} +</ul> 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 00000000..b4c9f1a1 --- /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 %} + <div class="form-group"> + {{ field.label_tag|add_class:"col-sm-2 control-label" }} + <div class="col-sm-10"> + {{ field|add_class:"form-control" }} + <span class="help-block">{{ field.help_text }}</span> + </div> + </div> +{% 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 00000000..e21a8e90 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/checkbox.html @@ -0,0 +1,14 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="true" {% if value %}checked{% endif %}> + {% if field.label %}{{ field.label }}{% endif %} + </label> + </div> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div>
\ 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 00000000..134cca66 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html @@ -0,0 +1,30 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + {% if style.inline %} + <div> + {% for key, text in field.choices.items %} + <label class="checkbox-inline"> + <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> + {{ text }} + </label> + {% endfor %} + </div> + {% else %} + {% for key, text in field.choices.items %} + <div class="checkbox"> + <label> + <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} + {% endif %} + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> 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 00000000..3eb5191c --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/fieldset.html @@ -0,0 +1,9 @@ +{% load rest_framework %} +<fieldset> + {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %} + {% for nested_field in field %} + {% if not nested_field.read_only %} + {% render_field nested_field style=style %} + {% endif %} + {% endfor %} +</fieldset> 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 00000000..e68835c0 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/form.html @@ -0,0 +1,11 @@ +{% load rest_framework %} +<form role="form" action="." method="POST" novalidate> + {% csrf_token %} + {% for field in form %} + {% if not field.read_only %} + {% render_field field style=style %} + {% endif %} + {% endfor %} + <!-- form.non_field_errors --> + <button type="submit" class="btn btn-default">Submit</button> +</form> 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 00000000..43cccd3e --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/input.html @@ -0,0 +1,12 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> 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 00000000..82d7b5f4 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html @@ -0,0 +1,4 @@ +<fieldset> + {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %} + <p>Lists are not currently supported in HTML input.</p> +</fieldset> 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 00000000..ed9f9ddb --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/radio.html @@ -0,0 +1,30 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + {% if style.inline %} + <div> + {% for key, text in field.choices.items %} + <label class="radio-inline"> + <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> + {{ text }} + </label> + {% endfor %} + </div> + {% else %} + {% for key, text in field.choices.items %} + <div class="radio"> + <label> + <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}> + {{ text }} + </label> + </div> + {% endfor %} + {% endif %} + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> 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 00000000..1d1109f6 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/select.html @@ -0,0 +1,19 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + <select class="form-control" name="{{ field.name }}"> + {% if field.allow_null or field.allow_blank %} + <option value="" {% if not field.value %}selected{% endif %}>--------</option> + {% endif %} + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option> + {% endfor %} + </select> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> 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 00000000..81b25c2a --- /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 %} + +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}"> + {% for key, text in field.choices.items %} + <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option> + {% empty %} + <option>{{ no_items }}</option> + {% endfor %} + </select> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> 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 00000000..840ea853 --- /dev/null +++ b/rest_framework/templates/rest_framework/vertical/textarea.html @@ -0,0 +1,12 @@ +<div class="form-group {% if field.errors %}has-error{% endif %}"> + {% if field.label %} + <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label> + {% endif %} + <textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea> + {% if field.errors %} + {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %} + {% endif %} + {% if field.help_text %} + <span class="help-block">{{ field.help_text }}</span> + {% endif %} +</div> diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index e9c1cdd5..bf0dc7b8 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() +# Regex for adding classes to html snippets +class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') -# 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)) +@register.simple_tag +def get_pagination_html(pager): + return pager.to_html() -# Regex for adding classes to html snippets -class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') +@register.simple_tag +def render_field(field, style=None): + style = style or {} + renderer = style.get('renderer', HTMLFormRenderer()) + return renderer.render_field(field, style) -# And the template tags themselves... @register.simple_tag def optional_login(request): @@ -121,22 +38,31 @@ def optional_login(request): except NoReverseMatch: return '' - snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path) + snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".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 '<li class="navbar-text">{user}</li>'.format(user=user) - snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path) - return snippet + snippet = """<li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown"> + {user} + <b class="caret"></b> + </a> + <ul class="dropdown-menu"> + <li><a href='{href}?next={next}'>Log out</a></li> + </ul> + </li>""" + + 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 a18f5a29..a83d082a 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/__init__.py b/rest_framework/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/rest_framework/tests/__init__.py +++ /dev/null diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py deleted file mode 100644 index b46d7f54..00000000 --- a/rest_framework/tests/description.py +++ /dev/null @@ -1,26 +0,0 @@ -# -- coding: utf-8 -- - -# Apparently there is a python 2.6 issue where docstrings of imported view classes -# do not retain their encoding information even if a module has a proper -# encoding declaration at the top of its source file. Therefore for tests -# to catch unicode related errors, a mock view has to be declared in a separate -# module. - -from rest_framework.views import APIView - - -# test strings snatched from http://www.columbia.edu/~fdc/utf8/, -# http://winrus.com/utf8-jap.htm and memory -UTF8_TEST_DOCSTRING = ( - 'zażółć gęślą jaźń' - 'Sîne klâwen durh die wolken sint geslagen' - 'Τη γλώσσα μου έδωσαν ελληνική' - 'யாமறிந்த மொழிகளிலே தமிழ்மொழி' - 'На берегу пустынных волн' - 'てすと' - 'アイウエオカキクケコサシスセソタチツテ' -) - - -class ViewWithNonASCIICharactersInDocstring(APIView): - __doc__ = UTF8_TEST_DOCSTRING diff --git a/rest_framework/tests/extras/__init__.py b/rest_framework/tests/extras/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/rest_framework/tests/extras/__init__.py +++ /dev/null diff --git a/rest_framework/tests/extras/bad_import.py b/rest_framework/tests/extras/bad_import.py deleted file mode 100644 index 68263d94..00000000 --- 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 1598ecd9..00000000 --- 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 a44813b6..00000000 --- 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 41ddf2ce..00000000 --- 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<key>[0-9]+)$', ResourceInstance.as_view()), - url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()), - url(r'^resource/(?P<key>[0-9]+)/(?P<other>[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_decorators.py b/rest_framework/tests/test_decorators.py deleted file mode 100644 index 195f0ba3..00000000 --- a/rest_framework/tests/test_decorators.py +++ /dev/null @@ -1,157 +0,0 @@ -from __future__ import unicode_literals -from django.test import TestCase -from rest_framework import status -from rest_framework.authentication import BasicAuthentication -from rest_framework.parsers import JSONParser -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.renderers import JSONRenderer -from rest_framework.test import APIRequestFactory -from rest_framework.throttling import UserRateThrottle -from rest_framework.views import APIView -from rest_framework.decorators import ( - api_view, - renderer_classes, - parser_classes, - authentication_classes, - throttle_classes, - permission_classes, -) - - -class DecoratorTestCase(TestCase): - - def setUp(self): - self.factory = APIRequestFactory() - - def _finalize_response(self, request, response, *args, **kwargs): - response.request = request - return APIView.finalize_response(self, request, response, *args, **kwargs) - - def test_api_view_incorrect(self): - """ - If @api_view is not applied correct, we should raise an assertion. - """ - - @api_view - def view(request): - return Response() - - request = self.factory.get('/') - self.assertRaises(AssertionError, view, request) - - def test_api_view_incorrect_arguments(self): - """ - If @api_view is missing arguments, we should raise an assertion. - """ - - with self.assertRaises(AssertionError): - @api_view('GET') - def view(request): - return Response() - - def test_calling_method(self): - - @api_view(['GET']) - def view(request): - return Response({}) - - request = self.factory.get('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - request = self.factory.post('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - - def test_calling_put_method(self): - - @api_view(['GET', 'PUT']) - def view(request): - return Response({}) - - request = self.factory.put('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - request = self.factory.post('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - - def test_calling_patch_method(self): - - @api_view(['GET', 'PATCH']) - def view(request): - return Response({}) - - request = self.factory.patch('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - request = self.factory.post('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - - def test_renderer_classes(self): - - @api_view(['GET']) - @renderer_classes([JSONRenderer]) - def view(request): - return Response({}) - - request = self.factory.get('/') - response = view(request) - self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer)) - - def test_parser_classes(self): - - @api_view(['GET']) - @parser_classes([JSONParser]) - def view(request): - self.assertEqual(len(request.parsers), 1) - self.assertTrue(isinstance(request.parsers[0], - JSONParser)) - return Response({}) - - request = self.factory.get('/') - view(request) - - def test_authentication_classes(self): - - @api_view(['GET']) - @authentication_classes([BasicAuthentication]) - def view(request): - self.assertEqual(len(request.authenticators), 1) - self.assertTrue(isinstance(request.authenticators[0], - BasicAuthentication)) - return Response({}) - - request = self.factory.get('/') - view(request) - - def test_permission_classes(self): - - @api_view(['GET']) - @permission_classes([IsAuthenticated]) - def view(request): - return Response({}) - - request = self.factory.get('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - - def test_throttle_classes(self): - class OncePerDayUserThrottle(UserRateThrottle): - rate = '1/day' - - @api_view(['GET']) - @throttle_classes([OncePerDayUserThrottle]) - def view(request): - return Response({}) - - request = self.factory.get('/') - response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - - response = view(request) - self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) diff --git a/rest_framework/tests/test_description.py b/rest_framework/tests/test_description.py deleted file mode 100644 index 8019f5ec..00000000 --- a/rest_framework/tests/test_description.py +++ /dev/null @@ -1,109 +0,0 @@ -# -- coding: utf-8 -- - -from __future__ import unicode_literals -from django.test import TestCase -from rest_framework.compat import apply_markdown, smart_text -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 - -# We check that docstrings get nicely un-indented. -DESCRIPTION = """an example docstring -==================== - -* list -* list - -another header --------------- - - code block - -indented - -# hash style header #""" - -# If markdown is installed we also test it's working -# (and that our wrapped forces '=' to h2 and '-' to h3) - -# We support markdown < 2.1 and markdown >= 2.1 -MARKED_DOWN_lt_21 = """<h2>an example docstring</h2> -<ul> -<li>list</li> -<li>list</li> -</ul> -<h3>another header</h3> -<pre><code>code block -</code></pre> -<p>indented</p> -<h2 id="hash_style_header">hash style header</h2>""" - -MARKED_DOWN_gte_21 = """<h2 id="an-example-docstring">an example docstring</h2> -<ul> -<li>list</li> -<li>list</li> -</ul> -<h3 id="another-header">another header</h3> -<pre><code>code block -</code></pre> -<p>indented</p> -<h2 id="hash-style-header">hash style header</h2>""" - - -class TestViewNamesAndDescriptions(TestCase): - def test_view_name_uses_class_name(self): - """ - Ensure view names are based on the class name. - """ - class MockView(APIView): - pass - self.assertEqual(get_view_name(MockView), 'Mock') - - def test_view_description_uses_docstring(self): - """Ensure view descriptions are based on the docstring.""" - class MockView(APIView): - """an example docstring - ==================== - - * list - * list - - another header - -------------- - - code block - - indented - - # hash style header #""" - - self.assertEqual(get_view_description(MockView), DESCRIPTION) - - def test_view_description_supports_unicode(self): - """ - Unicode in docstrings should be respected. - """ - - self.assertEqual( - get_view_description(ViewWithNonASCIICharactersInDocstring), - smart_text(UTF8_TEST_DOCSTRING) - ) - - def test_view_description_can_be_empty(self): - """ - Ensure that if a view has no docstring, - then it's description is the empty string. - """ - class MockView(APIView): - pass - self.assertEqual(get_view_description(MockView), '') - - def test_markdown(self): - """ - Ensure markdown to HTML works as expected. - """ - if apply_markdown: - gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 - lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 - self.assertTrue(gte_21_match or lt_21_match) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py deleted file mode 100644 index 6836ec86..00000000 --- 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 487046ac..00000000 --- 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 c9d9e7ff..00000000 --- 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<pk>\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_genericrelations.py b/rest_framework/tests/test_genericrelations.py deleted file mode 100644 index c38bfb9f..00000000 --- a/rest_framework/tests/test_genericrelations.py +++ /dev/null @@ -1,100 +0,0 @@ -from __future__ import unicode_literals -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 rest_framework import serializers - - -class Tag(models.Model): - """ - Tags have a descriptive slug, and are attached to an arbitrary object. - """ - tag = models.SlugField() - content_type = models.ForeignKey(ContentType) - object_id = models.PositiveIntegerField() - tagged_item = GenericForeignKey('content_type', 'object_id') - - def __unicode__(self): - return self.tag - - -class Bookmark(models.Model): - """ - A URL bookmark that may have multiple tags attached. - """ - url = models.URLField() - tags = GenericRelation(Tag) - - def __unicode__(self): - return 'Bookmark: %s' % self.url - - -class Note(models.Model): - """ - A textual note that may have multiple tags attached. - """ - text = models.TextField() - tags = GenericRelation(Tag) - - def __unicode__(self): - return 'Note: %s' % self.text - - -class TestGenericRelations(TestCase): - def setUp(self): - self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/') - Tag.objects.create(tagged_item=self.bookmark, tag='django') - Tag.objects.create(tagged_item=self.bookmark, tag='python') - self.note = Note.objects.create(text='Remember the milk') - Tag.objects.create(tagged_item=self.note, tag='reminder') - - def test_generic_relation(self): - """ - Test a relationship that spans a GenericRelation field. - IE. A reverse generic relationship. - """ - - class BookmarkSerializer(serializers.ModelSerializer): - tags = serializers.RelatedField(many=True) - - class Meta: - model = Bookmark - exclude = ('id',) - - serializer = BookmarkSerializer(self.bookmark) - expected = { - 'tags': ['django', 'python'], - 'url': 'https://www.djangoproject.com/' - } - self.assertEqual(serializer.data, expected) - - def test_generic_fk(self): - """ - Test a relationship that spans a GenericForeignKey field. - IE. A forward generic relationship. - """ - - class TagSerializer(serializers.ModelSerializer): - tagged_item = serializers.RelatedField() - - class Meta: - model = Tag - exclude = ('id', 'content_type', 'object_id') - - 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' - } - ] - self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py deleted file mode 100644 index 1550880b..00000000 --- a/rest_framework/tests/test_generics.py +++ /dev/null @@ -1,544 +0,0 @@ -from __future__ import unicode_literals -from django.db import models -from django.shortcuts import get_object_or_404 -from django.test import TestCase -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 - -factory = APIRequestFactory() - - -class RootView(generics.ListCreateAPIView): - """ - Example description for OPTIONS. - """ - model = BasicModel - - -class InstanceView(generics.RetrieveUpdateDestroyAPIView): - """ - Example description for OPTIONS. - """ - model = BasicModel - - -class SlugSerializer(serializers.ModelSerializer): - slug = serializers.Field() # read only - - class Meta: - model = SlugBasedModel - exclude = ('id',) - - -class SlugBasedInstanceView(InstanceView): - """ - A model with a slug-field. - """ - model = SlugBasedModel - serializer_class = SlugSerializer - lookup_field = 'slug' - - -class TestRootView(TestCase): - 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 = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = RootView.as_view() - - def test_get_root_view(self): - """ - GET requests to ListCreateAPIView should return list of objects. - """ - request = factory.get('/') - with self.assertNumQueries(1): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) - - def test_post_root_view(self): - """ - POST requests to ListCreateAPIView should create a new object. - """ - data = {'text': 'foobar'} - request = factory.post('/', data, format='json') - with self.assertNumQueries(1): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) - created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') - - def test_put_root_view(self): - """ - PUT requests to ListCreateAPIView should not be allowed - """ - data = {'text': 'foobar'} - request = factory.put('/', data, format='json') - 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."}) - - def test_delete_root_view(self): - """ - DELETE requests to ListCreateAPIView should not be allowed - """ - request = factory.delete('/') - 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) - - def test_post_cannot_set_id(self): - """ - POST requests to create a new object should not be able to set the id. - """ - data = {'id': 999, 'text': 'foobar'} - request = factory.post('/', data, format='json') - with self.assertNumQueries(1): - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) - created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') - - -class TestInstanceView(TestCase): - def setUp(self): - """ - Create 3 BasicModel intances. - """ - items = ['foo', 'bar', 'baz'] - for item in items: - BasicModel(text=item).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - self.view = InstanceView.as_view() - self.slug_based_view = SlugBasedInstanceView.as_view() - - def test_get_instance_view(self): - """ - GET requests to RetrieveUpdateDestroyAPIView should return a single object. - """ - request = factory.get('/1') - with self.assertNumQueries(1): - response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) - - def test_post_instance_view(self): - """ - POST requests to RetrieveUpdateDestroyAPIView should not be allowed - """ - data = {'text': 'foobar'} - request = factory.post('/', data, format='json') - 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."}) - - def test_put_instance_view(self): - """ - PUT requests to RetrieveUpdateDestroyAPIView should update an object. - """ - data = {'text': 'foobar'} - request = factory.put('/1', data, format='json') - with self.assertNumQueries(2): - response = self.view(request, pk='1').render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) - updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') - - def test_patch_instance_view(self): - """ - PATCH requests to RetrieveUpdateDestroyAPIView should update an object. - """ - data = {'text': 'foobar'} - request = factory.patch('/1', data, format='json') - - with self.assertNumQueries(2): - response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) - updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') - - def test_delete_instance_view(self): - """ - DELETE requests to RetrieveUpdateDestroyAPIView should delete an object. - """ - request = factory.delete('/1') - with self.assertNumQueries(2): - response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(response.content, six.b('')) - 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. - Regression test for #890. - """ - request = factory.get('/a') - with self.assertNumQueries(0): - response = self.view(request, pk='a').render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - - def test_put_cannot_set_id(self): - """ - PUT requests to create a new object should not be able to set the id. - """ - data = {'id': 999, 'text': 'foobar'} - request = factory.put('/1', data, format='json') - with self.assertNumQueries(2): - response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) - updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') - - def test_put_to_deleted_instance(self): - """ - PUT requests to RetrieveUpdateDestroyAPIView should create an object - if it does not currently exist. - """ - self.objects.get(id=1).delete() - data = {'text': 'foobar'} - request = factory.put('/1', data, format='json') - with self.assertNumQueries(3): - 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') - - def test_put_as_create_on_id_based_url(self): - """ - PUT requests to RetrieveUpdateDestroyAPIView should create an object - at the requested url if it doesn't exist. - """ - 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') - - def test_put_as_create_on_slug_based_url(self): - """ - PUT requests to RetrieveUpdateDestroyAPIView should create an object - at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. - """ - 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') - - -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. - """ - items = ['foo', 'bar', 'baz'] - for item in items: - BasicModel(text=item).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - - class OverriddenGetObjectView(generics.RetrieveUpdateDestroyAPIView): - """ - Example detail view for override of get_object(). - """ - model = BasicModel - - def get_object(self): - pk = int(self.kwargs['pk']) - return get_object_or_404(BasicModel.objects.all(), id=pk) - - self.view = OverriddenGetObjectView.as_view() - - def test_overridden_get_object_view(self): - """ - GET requests to RetrieveUpdateDestroyAPIView should return a single object. - """ - request = factory.get('/1') - with self.assertNumQueries(1): - response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) - - -# Regression test for #285 - -class CommentSerializer(serializers.ModelSerializer): - class Meta: - model = Comment - exclude = ('created',) - - -class CommentView(generics.ListCreateAPIView): - serializer_class = CommentSerializer - model = Comment - - -class TestCreateModelWithAutoNowAddField(TestCase): - def setUp(self): - self.objects = Comment.objects - self.view = CommentView.as_view() - - def test_create_model_with_auto_now_add_field(self): - """ - Regression test for #285 - - https://github.com/tomchristie/django-rest-framework/issues/285 - """ - data = {'email': 'foobar@example.com', 'content': 'foobar'} - request = factory.post('/', data, format='json') - response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - created = self.objects.get(id=1) - self.assertEqual(created.content, 'foobar') - - -# Test for particularly ugly regression with m2m in browsable API -class ClassB(models.Model): - name = models.CharField(max_length=255) - - -class ClassA(models.Model): - name = models.CharField(max_length=255) - childs = models.ManyToManyField(ClassB, blank=True, null=True) - - -class ClassASerializer(serializers.ModelSerializer): - childs = serializers.PrimaryKeyRelatedField(many=True, source='childs') - - class Meta: - model = ClassA - - -class ExampleView(generics.ListCreateAPIView): - serializer_class = ClassASerializer - model = ClassA - - -class TestM2MBrowseableAPI(TestCase): - def test_m2m_in_browseable_api(self): - """ - Test for particularly ugly regression with m2m in browsable API - """ - request = factory.get('/', HTTP_ACCEPT='text/html') - view = ExampleView().as_view() - response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - - -class InclusiveFilterBackend(object): - def filter_queryset(self, request, queryset, view): - return queryset.filter(text='foo') - - -class ExclusiveFilterBackend(object): - def filter_queryset(self, request, queryset, view): - return queryset.filter(text='other') - - -class TestFilterBackendAppliedToViews(TestCase): - - def setUp(self): - """ - Create 3 BasicModel instances to filter on. - """ - items = ['foo', 'bar', 'baz'] - for item in items: - BasicModel(text=item).save() - self.objects = BasicModel.objects - self.data = [ - {'id': obj.id, 'text': obj.text} - for obj in self.objects.all() - ] - - def test_get_root_view_filters_by_name_with_filter_backend(self): - """ - GET requests to ListCreateAPIView should return filtered list. - """ - root_view = RootView.as_view(filter_backends=(InclusiveFilterBackend,)) - request = factory.get('/') - response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}]) - - def test_get_root_view_filters_out_all_models_with_exclusive_filter_backend(self): - """ - GET requests to ListCreateAPIView should return empty list when all models are filtered out. - """ - root_view = RootView.as_view(filter_backends=(ExclusiveFilterBackend,)) - request = factory.get('/') - response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, []) - - def test_get_instance_view_filters_out_name_with_filter_backend(self): - """ - GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out. - """ - instance_view = InstanceView.as_view(filter_backends=(ExclusiveFilterBackend,)) - 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'}) - - def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): - """ - GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded - """ - instance_view = InstanceView.as_view(filter_backends=(InclusiveFilterBackend,)) - request = factory.get('/1') - response = instance_view(request, pk=1).render() - 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. - """ - view = DynamicSerializerView.as_view() - request = factory.get('/') - response = view(request).render() - self.assertContains(response, 'field_b') - self.assertNotContains(response, 'field_a') diff --git a/rest_framework/tests/test_htmlrenderer.py b/rest_framework/tests/test_htmlrenderer.py deleted file mode 100644 index 8957a43c..00000000 --- a/rest_framework/tests/test_htmlrenderer.py +++ /dev/null @@ -1,118 +0,0 @@ -from __future__ import unicode_literals -from django.core.exceptions import PermissionDenied -from django.http import Http404 -from django.test import TestCase -from django.template import TemplateDoesNotExist, Template -import django.template.loader -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 - - -@api_view(('GET',)) -@renderer_classes((TemplateHTMLRenderer,)) -def example(request): - """ - A view that can returns an HTML representation. - """ - data = {'object': 'foobar'} - return Response(data, template_name='example.html') - - -@api_view(('GET',)) -@renderer_classes((TemplateHTMLRenderer,)) -def permission_denied(request): - raise PermissionDenied() - - -@api_view(('GET',)) -@renderer_classes((TemplateHTMLRenderer,)) -def not_found(request): - raise Http404() - - -urlpatterns = patterns('', - url(r'^$', example), - url(r'^permission_denied$', permission_denied), - url(r'^not_found$', not_found), -) - - -class TemplateHTMLRendererTests(TestCase): - urls = 'rest_framework.tests.test_htmlrenderer' - - def setUp(self): - """ - Monkeypatch get_template - """ - self.get_template = django.template.loader.get_template - - def get_template(template_name): - if template_name == 'example.html': - return Template("example: {{ object }}") - raise TemplateDoesNotExist(template_name) - - django.template.loader.get_template = get_template - - def tearDown(self): - """ - Revert monkeypatching - """ - django.template.loader.get_template = self.get_template - - def test_simple_html_view(self): - response = self.client.get('/') - self.assertContains(response, "example: foobar") - self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') - - def test_not_found_html_view(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.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') - - def test_permission_denied_html_view(self): - response = self.client.get('/permission_denied') - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.content, six.b("403 Forbidden")) - self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') - - -class TemplateHTMLRendererExceptionTests(TestCase): - urls = 'rest_framework.tests.test_htmlrenderer' - - def setUp(self): - """ - Monkeypatch get_template - """ - self.get_template = django.template.loader.get_template - - def get_template(template_name): - if template_name == '404.html': - return Template("404: {{ detail }}") - if template_name == '403.html': - return Template("403: {{ detail }}") - raise TemplateDoesNotExist(template_name) - - django.template.loader.get_template = get_template - - def tearDown(self): - """ - Revert monkeypatching - """ - django.template.loader.get_template = self.get_template - - 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.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.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py deleted file mode 100644 index 61e613d7..00000000 --- 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<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'), - url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'), - url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'), - url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'), - url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'), - url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'), - url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'), - url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'), - url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'), - url(r'^optionalrelation/(?P<pk>\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_multitable_inheritance.py b/rest_framework/tests/test_multitable_inheritance.py deleted file mode 100644 index 00c15327..00000000 --- a/rest_framework/tests/test_multitable_inheritance.py +++ /dev/null @@ -1,67 +0,0 @@ -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 - - -# Models -class ParentModel(RESTFrameworkModel): - name1 = models.CharField(max_length=100) - - -class ChildModel(ParentModel): - name2 = models.CharField(max_length=100) - - -class AssociatedModel(RESTFrameworkModel): - ref = models.OneToOneField(ParentModel, primary_key=True) - name = models.CharField(max_length=100) - - -# Serializers -class DerivedModelSerializer(serializers.ModelSerializer): - class Meta: - model = ChildModel - - -class AssociatedModelSerializer(serializers.ModelSerializer): - class Meta: - model = AssociatedModel - - -# Tests -class IneritedModelSerializationTests(TestCase): - - def test_multitable_inherited_model_fields_as_expected(self): - """ - Assert that the parent pointer field is not included in the fields - serialized fields - """ - child = ChildModel(name1='parent name', name2='child name') - serializer = DerivedModelSerializer(child) - self.assertEqual(set(serializer.data.keys()), - set(['name1', 'name2', 'id'])) - - def test_onetoone_primary_key_model_fields_as_expected(self): - """ - 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) - serializer = AssociatedModelSerializer(associate) - self.assertEqual(set(serializer.data.keys()), - set(['name', 'ref'])) - - def test_data_is_valid_without_parent_ptr(self): - """ - Assert that the pointer to the parent table is not a required field - for input data - """ - data = { - 'name1': 'parent name', - 'name2': 'child name', - } - serializer = DerivedModelSerializer(data=data) - self.assertEqual(serializer.is_valid(), True) diff --git a/rest_framework/tests/test_negotiation.py b/rest_framework/tests/test_negotiation.py deleted file mode 100644 index 04b89eb6..00000000 --- a/rest_framework/tests/test_negotiation.py +++ /dev/null @@ -1,45 +0,0 @@ -from __future__ import unicode_literals -from django.test import TestCase -from rest_framework.negotiation import DefaultContentNegotiation -from rest_framework.request import Request -from rest_framework.renderers import BaseRenderer -from rest_framework.test import APIRequestFactory - - -factory = APIRequestFactory() - - -class MockJSONRenderer(BaseRenderer): - media_type = 'application/json' - - -class MockHTMLRenderer(BaseRenderer): - media_type = 'text/html' - - -class NoCharsetSpecifiedRenderer(BaseRenderer): - media_type = 'my/media' - - -class TestAcceptedMediaType(TestCase): - def setUp(self): - self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] - self.negotiator = DefaultContentNegotiation() - - def select_renderer(self, request): - return self.negotiator.select_renderer(request, self.renderers) - - def test_client_without_accept_use_renderer(self): - request = Request(factory.get('/')) - accepted_renderer, accepted_media_type = self.select_renderer(request) - self.assertEqual(accepted_media_type, 'application/json') - - def test_client_underspecifies_accept_use_renderer(self): - request = Request(factory.get('/', HTTP_ACCEPT='*/*')) - accepted_renderer, accepted_media_type = self.select_renderer(request) - self.assertEqual(accepted_media_type, 'application/json') - - def test_client_overspecifies_accept_use_client(self): - request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8')) - accepted_renderer, accepted_media_type = self.select_renderer(request) - self.assertEqual(accepted_media_type, 'application/json; indent=8') diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py deleted file mode 100644 index 85d4640e..00000000 --- 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 <fields>` - 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 7699e10c..00000000 --- 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( - '<?xml version="1.0" encoding="utf-8"?>' - '<root>' - '<field_a>121.0</field_a>' - '<field_b>dasd</field_b>' - '<field_c></field_c>' - '<field_d>2011-12-25 12:45:00</field_d>' - '</root>' - ) - 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( - '<?xml version="1.0" encoding="utf-8"?>' - '<root>' - '<creation_date>2011-12-25 12:45:00</creation_date>' - '<sub_data_list>' - '<list-item><sub_id>1</sub_id><sub_name>first</sub_name></list-item>' - '<list-item><sub_id>2</sub_id><sub_name>second</sub_name></list-item>' - '</sub_data_list>' - '<name>name</name>' - '</root>' - ) - 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 e2cca380..00000000 --- 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 d19219c9..00000000 --- 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_hyperlink.py b/rest_framework/tests/test_relations_hyperlink.py deleted file mode 100644 index 3c4d39af..00000000 --- a/rest_framework/tests/test_relations_hyperlink.py +++ /dev/null @@ -1,524 +0,0 @@ -from __future__ import unicode_literals -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, - ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, - NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource -) - -factory = APIRequestFactory() -request = factory.get('/') # Just to ensure we have a request in the serializer context - - -def dummy_view(request, pk): - pass - -urlpatterns = patterns('', - url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'), - url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'), - url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), - url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), - url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'), - url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'), - url(r'^onetoonetarget/(?P<pk>[0-9]+)/$', dummy_view, name='onetoonetarget-detail'), - url(r'^nullableonetoonesource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'), -) - - -# ManyToMany -class ManyToManyTargetSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ManyToManyTarget - fields = ('url', 'name', 'sources') - - -class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ManyToManySource - fields = ('url', 'name', 'targets') - - -# ForeignKey -class ForeignKeyTargetSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ForeignKeyTarget - fields = ('url', 'name', 'sources') - - -class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = ForeignKeySource - fields = ('url', 'name', 'target') - - -# Nullable ForeignKey -class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = NullableForeignKeySource - fields = ('url', 'name', 'target') - - -# Nullable OneToOne -class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = OneToOneTarget - fields = ('url', 'name', 'nullable_source') - - -# TODO: Add test that .data cannot be accessed prior to .is_valid - -class HyperlinkedManyToManyTests(TestCase): - urls = 'rest_framework.tests.test_relations_hyperlink' - - def setUp(self): - for idx in range(1, 4): - target = ManyToManyTarget(name='target-%d' % idx) - target.save() - source = ManyToManySource(name='source-%d' % idx) - source.save() - for target in ManyToManyTarget.objects.all(): - source.targets.add(target) - - def test_many_to_many_retrieve(self): - 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/']} - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_many_to_many_retrieve(self): - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, - {'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) - - 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/']} - instance = ManyToManySource.objects.get(pk=1) - serializer = ManyToManySourceSerializer(instance, data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure source 1 is updated, and everything else is as expected - 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/']} - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_many_to_many_update(self): - data = {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']} - instance = ManyToManyTarget.objects.get(pk=1) - serializer = ManyToManyTargetSerializer(instance, data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure target 1 is updated, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/']}, - {'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) - - def test_many_to_many_create(self): - data = {'url': 'http://testserver/manytomanysource/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']} - serializer = ManyToManySourceSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is added, and everything else is as expected - 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/4/', 'name': 'source-4', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/3/']} - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_many_to_many_create(self): - data = {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']} - serializer = ManyToManyTargetSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-4') - - # Ensure target 4 is added, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/manytomanytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']}, - {'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/']}, - {'url': 'http://testserver/manytomanytarget/4/', 'name': 'target-4', 'sources': ['http://testserver/manytomanysource/1/', 'http://testserver/manytomanysource/3/']} - ] - self.assertEqual(serializer.data, expected) - - -class HyperlinkedForeignKeyTests(TestCase): - urls = 'rest_framework.tests.test_relations_hyperlink' - - 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, context={'request': request}) - expected = [ - {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'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) - - def test_reverse_foreign_key_retrieve(self): - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'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) - - 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() - - # Ensure source 1 is updated, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}, - {'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) - - def test_foreign_key_update_incorrect_type(self): - data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 2} - 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.']}) - - 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/']} - instance = ForeignKeyTarget.objects.get(pk=2) - serializer = ForeignKeyTargetSerializer(instance, data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - # We shouldn't have saved anything to the db yet since save - # hasn't been called. - queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'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(new_serializer.data, expected) - - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure target 2 is update, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']}, - {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create(self): - data = {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'} - serializer = ForeignKeySourceSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 1 is updated, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'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/'}, - {'url': 'http://testserver/foreignkeysource/4/', 'name': 'source-4', 'target': 'http://testserver/foreignkeytarget/2/'}, - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_foreign_key_create(self): - data = {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']} - serializer = ForeignKeyTargetSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-3') - - # Ensure target 4 is added, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/2/']}, - {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []}, - {'url': 'http://testserver/foreignkeytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_invalid_null(self): - data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': None} - 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.']}) - - -class HyperlinkedNullableForeignKeyTests(TestCase): - urls = 'rest_framework.tests.test_relations_hyperlink' - - 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, context={'request': request}) - expected = [ - {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_null(self): - data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': ''} - expected_data = {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data, context={'request': request}) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, expected_data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - {'url': 'http://testserver/nullableforeignkeysource/4/', 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_null(self): - data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} - 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() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, - {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': ''} - expected_data = {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None} - 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() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True, context={'request': request}) - expected = [ - {'url': 'http://testserver/nullableforeignkeysource/1/', 'name': 'source-1', 'target': None}, - {'url': 'http://testserver/nullableforeignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'}, - {'url': 'http://testserver/nullableforeignkeysource/3/', 'name': 'source-3', 'target': None}, - ] - 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' - - 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, context={'request': request}) - expected = [ - {'url': 'http://testserver/onetoonetarget/1/', 'name': 'target-1', 'nullable_source': 'http://testserver/nullableonetoonesource/1/'}, - {'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_nested.py b/rest_framework/tests/test_relations_nested.py deleted file mode 100644 index f6d006b3..00000000 --- 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_relations_pk.py b/rest_framework/tests/test_relations_pk.py deleted file mode 100644 index e2a1b815..00000000 --- a/rest_framework/tests/test_relations_pk.py +++ /dev/null @@ -1,542 +0,0 @@ -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, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, - NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource, -) -from rest_framework.compat import six - - -# ManyToMany -class ManyToManyTargetSerializer(serializers.ModelSerializer): - class Meta: - model = ManyToManyTarget - fields = ('id', 'name', 'sources') - - -class ManyToManySourceSerializer(serializers.ModelSerializer): - class Meta: - model = ManyToManySource - fields = ('id', 'name', 'targets') - - -# ForeignKey -class ForeignKeyTargetSerializer(serializers.ModelSerializer): - class Meta: - model = ForeignKeyTarget - fields = ('id', 'name', 'sources') - - -class ForeignKeySourceSerializer(serializers.ModelSerializer): - class Meta: - model = ForeignKeySource - fields = ('id', 'name', 'target') - - -# Nullable ForeignKey -class NullableForeignKeySourceSerializer(serializers.ModelSerializer): - class Meta: - model = NullableForeignKeySource - fields = ('id', 'name', 'target') - - -# Nullable OneToOne -class NullableOneToOneTargetSerializer(serializers.ModelSerializer): - class Meta: - model = OneToOneTarget - fields = ('id', 'name', 'nullable_source') - - -# TODO: Add test that .data cannot be accessed prior to .is_valid - -class PKManyToManyTests(TestCase): - def setUp(self): - for idx in range(1, 4): - target = ManyToManyTarget(name='target-%d' % idx) - target.save() - source = ManyToManySource(name='source-%d' % idx) - source.save() - for target in ManyToManyTarget.objects.all(): - source.targets.add(target) - - def test_many_to_many_retrieve(self): - 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]} - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_many_to_many_retrieve(self): - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]} - ] - self.assertEqual(serializer.data, expected) - - def test_many_to_many_update(self): - data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} - instance = ManyToManySource.objects.get(pk=1) - serializer = ManyToManySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure source 1 is updated, and everything else is as expected - 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]} - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_many_to_many_update(self): - data = {'id': 1, 'name': 'target-1', 'sources': [1]} - instance = ManyToManyTarget.objects.get(pk=1) - serializer = ManyToManyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure target 1 is updated, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]} - ] - self.assertEqual(serializer.data, expected) - - def test_many_to_many_create(self): - data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]} - serializer = ManyToManySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # 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]}, - {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}, - {'id': 4, 'name': 'source-4', 'targets': [1, 3]}, - ] - 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) - self.assertEqual(obj.name, 'target-4') - - # Ensure target 4 is added, and everything else is as expected - queryset = ManyToManyTarget.objects.all() - serializer = ManyToManyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, - {'id': 3, 'name': 'target-3', 'sources': [3]}, - {'id': 4, 'name': 'target-4', 'sources': [1, 3]} - ] - self.assertEqual(serializer.data, expected) - - -class PKForeignKeyTests(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': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', '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': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': []}, - ] - self.assertEqual(serializer.data, expected) - - 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() - - # Ensure source 1 is updated, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 2}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': 1} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_incorrect_type(self): - data = {'id': 1, 'name': 'source-1', 'target': 'foo'} - 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__]}) - - def test_reverse_foreign_key_update(self): - data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} - instance = ForeignKeyTarget.objects.get(pk=2) - serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - # We shouldn't have saved anything to the db yet since save - # hasn't been called. - queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, - {'id': 2, 'name': 'target-2', 'sources': []}, - ] - self.assertEqual(new_serializer.data, expected) - - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure target 2 is update, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [2]}, - {'id': 2, 'name': 'target-2', 'sources': [1, 3]}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create(self): - data = {'id': 4, 'name': 'source-4', 'target': 2} - serializer = ForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is added, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': 1}, - {'id': 4, 'name': 'source-4', 'target': 2}, - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_foreign_key_create(self): - data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]} - serializer = ForeignKeyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-3') - - # Ensure target 3 is added, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': [2]}, - {'id': 2, 'name': 'target-2', 'sources': []}, - {'id': 3, 'name': 'target-3', 'sources': [1, 3]}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_invalid_null(self): - data = {'id': 1, 'name': 'source-1', 'target': None} - 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.']}) - - -class PKNullableForeignKeyTests(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': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': None}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_null(self): - data = {'id': 4, 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': None}, - {'id': 4, 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'id': 4, 'name': 'source-4', 'target': ''} - expected_data = {'id': 4, 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, expected_data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 1}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': None}, - {'id': 4, 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_null(self): - data = {'id': 1, 'name': 'source-1', 'target': None} - instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.data, data) - serializer.save() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': None}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'id': 1, 'name': 'source-1', 'target': ''} - expected_data = {'id': 1, 'name': 'source-1', 'target': None} - instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.data, expected_data) - serializer.save() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': None}, - {'id': 2, 'name': 'source-2', 'target': 1}, - {'id': 3, 'name': 'source-3', 'target': None} - ] - 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): - target = OneToOneTarget(name='target-1') - target.save() - new_target = OneToOneTarget(name='target-2') - new_target.save() - source = NullableOneToOneSource(name='source-1', target=new_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': None}, - {'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/rest_framework/tests/test_relations_slug.py deleted file mode 100644 index 435c821c..00000000 --- a/rest_framework/tests/test_relations_slug.py +++ /dev/null @@ -1,257 +0,0 @@ -from django.test import TestCase -from rest_framework import serializers -from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget - - -class ForeignKeyTargetSerializer(serializers.ModelSerializer): - sources = serializers.SlugRelatedField(many=True, slug_field='name') - - class Meta: - model = ForeignKeyTarget - - -class ForeignKeySourceSerializer(serializers.ModelSerializer): - target = serializers.SlugRelatedField(slug_field='name') - - class Meta: - model = ForeignKeySource - - -class NullableForeignKeySourceSerializer(serializers.ModelSerializer): - target = serializers.SlugRelatedField(slug_field='name', required=False) - - class Meta: - model = NullableForeignKeySource - - -# TODO: M2M Tests, FKTests (Non-nullable), One2One -class SlugForeignKeyTests(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': 'target-1'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': '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': ['source-1', 'source-2', 'source-3']}, - {'id': 2, 'name': 'target-2', 'sources': []}, - ] - self.assertEqual(serializer.data, expected) - - 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() - - # Ensure source 1 is updated, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 'target-2'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': 'target-1'} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_incorrect_type(self): - data = {'id': 1, 'name': 'source-1', 'target': 123} - instance = ForeignKeySource.objects.get(pk=1) - serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target': ['Object with name=123 does not exist.']}) - - def test_reverse_foreign_key_update(self): - data = {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']} - instance = ForeignKeyTarget.objects.get(pk=2) - serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - # We shouldn't have saved anything to the db yet since save - # hasn't been called. - queryset = ForeignKeyTarget.objects.all() - new_serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': ['source-1', 'source-2', 'source-3']}, - {'id': 2, 'name': 'target-2', 'sources': []}, - ] - self.assertEqual(new_serializer.data, expected) - - serializer.save() - self.assertEqual(serializer.data, data) - - # Ensure target 2 is update, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, - {'id': 2, 'name': 'target-2', 'sources': ['source-1', 'source-3']}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create(self): - data = {'id': 4, 'name': 'source-4', 'target': 'target-2'} - serializer = ForeignKeySourceSerializer(data=data) - serializer.is_valid() - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is added, and everything else is as expected - queryset = ForeignKeySource.objects.all() - serializer = ForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 'target-1'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': 'target-1'}, - {'id': 4, 'name': 'source-4', 'target': 'target-2'}, - ] - self.assertEqual(serializer.data, expected) - - def test_reverse_foreign_key_create(self): - data = {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']} - serializer = ForeignKeyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-3') - - # Ensure target 3 is added, and everything else is as expected - queryset = ForeignKeyTarget.objects.all() - serializer = ForeignKeyTargetSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'target-1', 'sources': ['source-2']}, - {'id': 2, 'name': 'target-2', 'sources': []}, - {'id': 3, 'name': 'target-3', 'sources': ['source-1', 'source-3']}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_invalid_null(self): - data = {'id': 1, 'name': 'source-1', 'target': None} - 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.']}) - - -class SlugNullableForeignKeyTests(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': 'target-1'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': None}, - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_null(self): - data = {'id': 4, 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 'target-1'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': None}, - {'id': 4, 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_create_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'id': 4, 'name': 'source-4', 'target': ''} - expected_data = {'id': 4, 'name': 'source-4', 'target': None} - serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) - obj = serializer.save() - self.assertEqual(serializer.data, expected_data) - self.assertEqual(obj.name, 'source-4') - - # Ensure source 4 is created, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': 'target-1'}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': None}, - {'id': 4, 'name': 'source-4', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_null(self): - data = {'id': 1, 'name': 'source-1', 'target': None} - instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.data, data) - serializer.save() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': None}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': None} - ] - self.assertEqual(serializer.data, expected) - - def test_foreign_key_update_with_valid_emptystring(self): - """ - The emptystring should be interpreted as null in the context - of relationships. - """ - data = {'id': 1, 'name': 'source-1', 'target': ''} - expected_data = {'id': 1, 'name': 'source-1', 'target': None} - instance = NullableForeignKeySource.objects.get(pk=1) - serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.data, expected_data) - serializer.save() - - # Ensure source 1 is updated, and everything else is as expected - queryset = NullableForeignKeySource.objects.all() - serializer = NullableForeignKeySourceSerializer(queryset, many=True) - expected = [ - {'id': 1, 'name': 'source-1', 'target': None}, - {'id': 2, 'name': 'source-2', 'target': 'target-1'}, - {'id': 3, 'name': 'source-3', 'target': None} - ] - self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py deleted file mode 100644 index df6f4aa6..00000000 --- a/rest_framework/tests/test_renderers.py +++ /dev/null @@ -1,539 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from decimal import Decimal -from django.core.cache import cache -from django.test import TestCase -from django.utils import unittest -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.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.settings import api_settings -from rest_framework.test import APIRequestFactory -import datetime -import pickle -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') - - -expected_results = [ - ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator -] - - -class BasicRendererTests(TestCase): - def test_expected_results(self): - for value, renderer_cls, expected in expected_results: - output = renderer_cls().render(value) - self.assertEqual(output, expected) - - -class RendererA(BaseRenderer): - media_type = 'mock/renderera' - format = "formata" - - def render(self, data, media_type=None, renderer_context=None): - return RENDERER_A_SERIALIZER(data) - - -class RendererB(BaseRenderer): - media_type = 'mock/rendererb' - format = "formatb" - - def render(self, data, media_type=None, renderer_context=None): - return RENDERER_B_SERIALIZER(data) - - -class MockView(APIView): - renderer_classes = (RendererA, RendererB) - - def get(self, request, **kwargs): - response = Response(DUMMYCONTENT, status=DUMMYSTATUS) - return response - - -class MockGETView(APIView): - - def get(self, request, **kwargs): - return Response({'foo': ['bar', 'baz']}) - - -class HTMLView(APIView): - renderer_classes = (BrowsableAPIRenderer, ) - - def get(self, request, **kwargs): - return Response('text') - - -class HTMLView1(APIView): - renderer_classes = (BrowsableAPIRenderer, JSONRenderer) - - def get(self, request, **kwargs): - return Response('text') - -urlpatterns = patterns('', - url(r'^.*\.(?P<format>.+)$', 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'^html$', HTMLView.as_view()), - url(r'^html1$', HTMLView1.as_view()), - url(r'^api', include('rest_framework.urls', namespace='rest_framework')) -) - - -class POSTDeniedPermission(permissions.BasePermission): - def has_permission(self, request, view): - return request.method != 'POST' - - -class POSTDeniedView(APIView): - renderer_classes = (BrowsableAPIRenderer,) - permission_classes = (POSTDeniedPermission,) - - def get(self, request): - return Response() - - def post(self, request): - return Response() - - def put(self, request): - return Response() - - def patch(self, request): - return Response() - - -class DocumentingRendererTests(TestCase): - def test_only_permitted_forms_are_displayed(self): - view = POSTDeniedView.as_view() - request = APIRequestFactory().get('/') - response = view(request).render() - self.assertNotContains(response, '>POST<') - self.assertContains(response, '>PUT<') - self.assertContains(response, '>PATCH<') - - -class RendererEndToEndTests(TestCase): - """ - End-to-end testing of renderers using an RendererMixin on a generic view. - """ - - urls = 'rest_framework.tests.test_renderers' - - def test_default_renderer_serializes_content(self): - """If the Accept header is not set the default renderer should serialize the response.""" - resp = self.client.get('/') - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_head_method_serializes_no_content(self): - """No response must be included in HEAD requests.""" - resp = self.client.head('/') - self.assertEqual(resp.status_code, DUMMYSTATUS) - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, six.b('')) - - def test_default_renderer_serializes_content_on_accept_any(self): - """If the Accept header is set to */* the default renderer should serialize the response.""" - resp = self.client.get('/', HTTP_ACCEPT='*/*') - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_default_case(self): - """If the Accept header is set the specified renderer should serialize the response. - (In this case we check that works for the default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_non_default_case(self): - """If the Accept header is set the specified renderer should serialize the response. - (In this case we check that works for a non-default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_on_accept_query(self): - """The '_accept' query string should behave in the same way as the Accept header.""" - param = '?%s=%s' % ( - api_settings.URL_ACCEPT_OVERRIDE, - RendererB.media_type - ) - resp = self.client.get('/' + param) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_unsatisfiable_accept_header_on_request_returns_406_status(self): - """If the Accept header is unsatisfiable we should return a 406 Not Acceptable response.""" - resp = self.client.get('/', HTTP_ACCEPT='foo/bar') - self.assertEqual(resp.status_code, status.HTTP_406_NOT_ACCEPTABLE) - - def test_specified_renderer_serializes_content_on_format_query(self): - """If a 'format' query is specified, the renderer with the matching - format attribute should serialize the response.""" - param = '?%s=%s' % ( - api_settings.URL_FORMAT_OVERRIDE, - RendererB.format - ) - resp = self.client.get('/' + param) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_on_format_kwargs(self): - """If a 'format' keyword arg is specified, the renderer with the matching - format attribute should serialize the response.""" - resp = self.client.get('/something.formatb') - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): - """If both a 'format' query and a matching Accept header specified, - the renderer with the matching format attribute should serialize the response.""" - param = '?%s=%s' % ( - api_settings.URL_FORMAT_OVERRIDE, - RendererB.format - ) - resp = self.client.get('/' + param, - HTTP_ACCEPT=RendererB.media_type) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - -_flat_repr = '{"foo": ["bar", "baz"]}' -_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' - - -def strip_trailing_whitespace(content): - """ - Seems to be some inconsistencies re. trailing whitespace with - different versions of the json lib. - """ - return re.sub(' +\n', '\n', content) - - -class JSONRendererTests(TestCase): - """ - Tests specific to the JSON Renderer - """ - - def test_render_lazy_strings(self): - """ - JSONRenderer should deal with lazy translated strings. - """ - ret = JSONRenderer().render(_('test')) - self.assertEqual(ret, b'"test"') - - def test_without_content_type_args(self): - """ - Test basic JSON rendering. - """ - obj = {'foo': ['bar', 'baz']} - renderer = JSONRenderer() - content = renderer.render(obj, 'application/json') - # Fix failing test case which depends on version of JSON library. - self.assertEqual(content.decode('utf-8'), _flat_repr) - - def test_with_content_type_args(self): - """ - Test JSON rendering with additional content type arguments supplied. - """ - obj = {'foo': ['bar', 'baz']} - renderer = JSONRenderer() - 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): - """ - Tests specific for the Unicode JSON Renderer - """ - def test_proper_encoding(self): - obj = {'countries': ['United Kingdom', 'France', 'España']} - renderer = UnicodeJSONRenderer() - content = renderer.render(obj, 'application/json') - self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8')) - - -class JSONPRendererTests(TestCase): - """ - Tests specific to the JSONP 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, '<field>astring</field>') - - def test_render_integer(self): - """ - Test XML rendering. - """ - renderer = XMLRenderer() - content = renderer.render({'field': 111}, 'application/xml') - self.assertXMLContains(content, '<field>111</field>') - - 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, '<field>2011-12-25 12:45:00</field>') - - def test_render_float(self): - """ - Test XML rendering. - """ - renderer = XMLRenderer() - content = renderer.render({'field': 123.4}, 'application/xml') - self.assertXMLContains(content, '<field>123.4</field>') - - def test_render_decimal(self): - """ - Test XML rendering. - """ - renderer = XMLRenderer() - content = renderer.render({'field': Decimal('111.2')}, 'application/xml') - self.assertXMLContains(content, '<field>111.2</field>') - - def test_render_none(self): - """ - Test XML rendering. - """ - renderer = XMLRenderer() - content = renderer.render({'field': None}, 'application/xml') - self.assertXMLContains(content, '<field></field>') - - def test_render_complex_data(self): - """ - Test XML rendering. - """ - renderer = XMLRenderer() - content = renderer.render(self._complex_data, 'application/xml') - self.assertXMLContains(content, '<sub_name>first</sub_name>') - self.assertXMLContains(content, '<sub_name>second</sub_name>') - - @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('<?xml version="1.0" encoding="utf-8"?>\n<root>')) - self.assertTrue(xml.endswith('</root>')) - self.assertTrue(string in xml, '%r not in %r' % (string, xml)) - - -# Tests for caching issue, #346 -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) - - 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) - - def test_get_caching(self): - """ - Test caching of GET requests - """ - resp = self.http_resp('get', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) - self.assertEqual(cached_resp.content, resp.content) diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py deleted file mode 100644 index 969d8024..00000000 --- a/rest_framework/tests/test_request.py +++ /dev/null @@ -1,314 +0,0 @@ -""" -Tests for content parsing, and form-overloaded content parsing. -""" -from __future__ import unicode_literals -from django.contrib.auth.models import User -from django.contrib.auth import authenticate, login, logout -from django.contrib.sessions.middleware import SessionMiddleware -from django.test import TestCase -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.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 -import json - - -factory = APIRequestFactory() - - -class PlainTextParser(BaseParser): - media_type = 'text/plain' - - def parse(self, stream, media_type=None, parser_context=None): - """ - Returns a 2-tuple of `(data, files)`. - - `data` will simply be a string representing the body of the request. - `files` will always be `None`. - """ - return stream.read() - - -class TestMethodOverloading(TestCase): - def test_method(self): - """ - Request methods should be same as underlying request. - """ - request = Request(factory.get('/')) - self.assertEqual(request.method, 'GET') - request = Request(factory.post('/')) - self.assertEqual(request.method, 'POST') - - def test_overloaded_method(self): - """ - POST requests can be overloaded to another method by setting a - reserved form field - """ - request = Request(factory.post('/', {api_settings.FORM_METHOD_OVERRIDE: 'DELETE'})) - self.assertEqual(request.method, 'DELETE') - - def test_x_http_method_override_header(self): - """ - POST requests can also be overloaded to another method by setting - the X-HTTP-Method-Override header. - """ - request = Request(factory.post('/', {'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): - """ - Ensure request.DATA returns empty QueryDict for GET request. - """ - request = Request(factory.get('/')) - self.assertEqual(request.DATA, {}) - - def test_standard_behaviour_determines_no_content_HEAD(self): - """ - Ensure request.DATA returns empty QueryDict for HEAD request. - """ - request = Request(factory.head('/')) - self.assertEqual(request.DATA, {}) - - def test_request_DATA_with_form_content(self): - """ - Ensure request.DATA returns content for POST request with form content. - """ - data = {'qwerty': 'uiop'} - request = Request(factory.post('/', data)) - request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(list(request.DATA.items()), list(data.items())) - - def test_request_DATA_with_text_content(self): - """ - Ensure request.DATA returns content for POST request with - non-form content. - """ - content = six.b('qwerty') - content_type = 'text/plain' - request = Request(factory.post('/', content, content_type=content_type)) - request.parsers = (PlainTextParser(),) - self.assertEqual(request.DATA, content) - - def test_request_POST_with_form_content(self): - """ - Ensure request.POST returns content for POST request with form content. - """ - data = {'qwerty': 'uiop'} - request = Request(factory.post('/', data)) - request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(list(request.POST.items()), list(data.items())) - - def test_standard_behaviour_determines_form_content_PUT(self): - """ - Ensure request.DATA returns content for PUT request with form content. - """ - data = {'qwerty': 'uiop'} - request = Request(factory.put('/', data)) - request.parsers = (FormParser(), MultiPartParser()) - self.assertEqual(list(request.DATA.items()), list(data.items())) - - def test_standard_behaviour_determines_non_form_content_PUT(self): - """ - Ensure request.DATA returns content for PUT request with - non-form content. - """ - content = six.b('qwerty') - content_type = 'text/plain' - request = Request(factory.put('/', content, content_type=content_type)) - request.parsers = (PlainTextParser(), ) - self.assertEqual(request.DATA, content) - - def test_overloaded_behaviour_allows_content_tunnelling(self): - """ - Ensure request.DATA returns content for overloaded POST request. - """ - json_data = {'foobar': 'qwerty'} - content = json.dumps(json_data) - content_type = 'application/json' - form_data = { - api_settings.FORM_CONTENT_OVERRIDE: content, - api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type - } - request = Request(factory.post('/', form_data)) - 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()) - - -class MockView(APIView): - authentication_classes = (SessionAuthentication,) - - def post(self, request): - if request.POST.get('example') is not None: - return Response(status=status.HTTP_200_OK) - - return Response(status=status.INTERNAL_SERVER_ERROR) - -urlpatterns = patterns('', - (r'^$', MockView.as_view()), -) - - -class TestContentParsingWithAuthentication(TestCase): - urls = 'rest_framework.tests.test_request' - - 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_user_logged_in_authentication_has_POST_when_not_logged_in(self): - """ - Ensures request.POST exists after SessionAuthentication when user - doesn't log in. - """ - content = {'example': 'example'} - - response = self.client.post('/', content) - self.assertEqual(status.HTTP_200_OK, response.status_code) - - 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('/')) - SessionMiddleware().process_request(self.request) - - User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow') - self.user = authenticate(username='ringo', password='yellow') - - def test_user_can_be_set(self): - self.request.user = self.user - self.assertEqual(self.request.user, self.user) - - def test_user_can_login(self): - login(self.request, self.user) - self.assertEqual(self.request.user, self.user) - - def test_user_can_logout(self): - self.request.user = self.user - self.assertFalse(self.request.user.is_anonymous()) - logout(self.request) - self.assertTrue(self.request.user.is_anonymous()) - - -class TestAuthSetter(TestCase): - - def test_auth_can_be_set(self): - request = Request(factory.get('/')) - request.auth = 'DUMMY' - self.assertEqual(request.auth, 'DUMMY') diff --git a/rest_framework/tests/test_response.py b/rest_framework/tests/test_response.py deleted file mode 100644 index eea3c641..00000000 --- a/rest_framework/tests/test_response.py +++ /dev/null @@ -1,278 +0,0 @@ -from __future__ import unicode_literals -from django.test import TestCase -from rest_framework.tests.models import BasicModel, BasicModelSerializer -from rest_framework.compat import patterns, url, include -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 status -from rest_framework.renderers import ( - BaseRenderer, - JSONRenderer, - BrowsableAPIRenderer -) -from rest_framework import viewsets -from rest_framework.settings import api_settings -from rest_framework.compat import six - - -class MockPickleRenderer(BaseRenderer): - media_type = 'application/pickle' - - -class MockJsonRenderer(BaseRenderer): - media_type = 'application/json' - - -class MockTextMediaRenderer(BaseRenderer): - media_type = 'text/html' - -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') - - -class RendererA(BaseRenderer): - media_type = 'mock/renderera' - format = "formata" - - def render(self, data, media_type=None, renderer_context=None): - return RENDERER_A_SERIALIZER(data) - - -class RendererB(BaseRenderer): - media_type = 'mock/rendererb' - format = "formatb" - - def render(self, data, media_type=None, renderer_context=None): - return RENDERER_B_SERIALIZER(data) - - -class RendererC(RendererB): - media_type = 'mock/rendererc' - format = 'formatc' - charset = "rendererc" - - -class MockView(APIView): - renderer_classes = (RendererA, RendererB, RendererC) - - def get(self, request, **kwargs): - return Response(DUMMYCONTENT, status=DUMMYSTATUS) - - -class MockViewSettingContentType(APIView): - renderer_classes = (RendererA, RendererB, RendererC) - - def get(self, request, **kwargs): - return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') - - -class HTMLView(APIView): - renderer_classes = (BrowsableAPIRenderer, ) - - def get(self, request, **kwargs): - return Response('text') - - -class HTMLView1(APIView): - renderer_classes = (BrowsableAPIRenderer, JSONRenderer) - - def get(self, request, **kwargs): - return Response('text') - - -class HTMLNewModelViewSet(viewsets.ModelViewSet): - model = BasicModel - - -class HTMLNewModelView(generics.ListCreateAPIView): - renderer_classes = (BrowsableAPIRenderer,) - permission_classes = [] - serializer_class = BasicModelSerializer - model = BasicModel - - -new_model_viewset_router = routers.DefaultRouter() -new_model_viewset_router.register(r'', HTMLNewModelViewSet) - - -urlpatterns = patterns('', - url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])), - url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), - url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])), - url(r'^html$', HTMLView.as_view()), - url(r'^html1$', HTMLView1.as_view()), - url(r'^html_new_model$', HTMLNewModelView.as_view()), - url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)), - url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) -) - - -# TODO: Clean tests bellow - remove duplicates with above, better unit testing, ... -class RendererIntegrationTests(TestCase): - """ - End-to-end testing of renderers using an ResponseMixin on a generic view. - """ - - urls = 'rest_framework.tests.test_response' - - def test_default_renderer_serializes_content(self): - """If the Accept header is not set the default renderer should serialize the response.""" - resp = self.client.get('/') - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_head_method_serializes_no_content(self): - """No response must be included in HEAD requests.""" - resp = self.client.head('/') - self.assertEqual(resp.status_code, DUMMYSTATUS) - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, six.b('')) - - def test_default_renderer_serializes_content_on_accept_any(self): - """If the Accept header is set to */* the default renderer should serialize the response.""" - resp = self.client.get('/', HTTP_ACCEPT='*/*') - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_default_case(self): - """If the Accept header is set the specified renderer should serialize the response. - (In this case we check that works for the default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type) - self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_non_default_case(self): - """If the Accept header is set the specified renderer should serialize the response. - (In this case we check that works for a non-default renderer)""" - resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_on_accept_query(self): - """The '_accept' query string should behave in the same way as the Accept header.""" - param = '?%s=%s' % ( - api_settings.URL_ACCEPT_OVERRIDE, - RendererB.media_type - ) - resp = self.client.get('/' + param) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_on_format_query(self): - """If a 'format' query is specified, the renderer with the matching - format attribute should serialize the response.""" - resp = self.client.get('/?format=%s' % RendererB.format) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_serializes_content_on_format_kwargs(self): - """If a 'format' keyword arg is specified, the renderer with the matching - format attribute should serialize the response.""" - resp = self.client.get('/something.formatb') - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - def test_specified_renderer_is_used_on_format_query_with_matching_accept(self): - """If both a 'format' query and a matching Accept header specified, - the renderer with the matching format attribute should serialize the response.""" - resp = self.client.get('/?format=%s' % RendererB.format, - HTTP_ACCEPT=RendererB.media_type) - self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8') - self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) - self.assertEqual(resp.status_code, DUMMYSTATUS) - - -class Issue122Tests(TestCase): - """ - Tests that covers #122. - """ - urls = 'rest_framework.tests.test_response' - - def test_only_html_renderer(self): - """ - Test if no infinite recursion occurs. - """ - self.client.get('/html') - - def test_html_renderer_is_first(self): - """ - Test if no infinite recursion occurs. - """ - self.client.get('/html1') - - -class Issue467Tests(TestCase): - """ - Tests for #467 - """ - - urls = 'rest_framework.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.') - - -class Issue807Tests(TestCase): - """ - Covers #807 - """ - - urls = 'rest_framework.tests.test_response' - - def test_does_not_append_charset_by_default(self): - """ - Renderers don't include a charset unless set explicitly. - """ - headers = {"HTTP_ACCEPT": RendererA.media_type} - resp = self.client.get('/', **headers) - expected = "{0}; charset={1}".format(RendererA.media_type, 'utf-8') - self.assertEqual(expected, resp['Content-Type']) - - def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self): - """ - If renderer class has charset attribute declared, it gets appended - to Response's Content-Type - """ - headers = {"HTTP_ACCEPT": RendererC.media_type} - resp = self.client.get('/', **headers) - 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): - """ - The content type may be set explictly on the response. - """ - headers = {"HTTP_ACCEPT": RendererC.media_type} - resp = self.client.get('/setbyview', **headers) - self.assertEqual('setbyview', resp['Content-Type']) - - def test_viewset_label_help_text(self): - param = '?%s=%s' % ( - api_settings.URL_ACCEPT_OVERRIDE, - 'text/html' - ) - 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.') - - 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.') diff --git a/rest_framework/tests/test_reverse.py b/rest_framework/tests/test_reverse.py deleted file mode 100644 index 690a30b1..00000000 --- a/rest_framework/tests/test_reverse.py +++ /dev/null @@ -1,27 +0,0 @@ -from __future__ import unicode_literals -from django.test import TestCase -from rest_framework.compat import patterns, url -from rest_framework.reverse import reverse -from rest_framework.test import APIRequestFactory - -factory = APIRequestFactory() - - -def null_view(request): - pass - -urlpatterns = patterns('', - url(r'^view$', null_view, name='view'), -) - - -class ReverseTests(TestCase): - """ - Tests for fully qualified URLs when using `reverse`. - """ - urls = 'rest_framework.tests.test_reverse' - - def test_reversed_urls_are_fully_qualified(self): - request = factory.get('/view') - url = reverse('view', request=request) - self.assertEqual(url, 'http://testserver/view') diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py deleted file mode 100644 index 5fcccb74..00000000 --- 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('<uuid>', 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<pk>[^/]+)/$'] - 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<pk>[^/]+)$'] - 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 c2497660..00000000 --- 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 8b0ded1a..00000000 --- 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 71d0e24b..00000000 --- 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 857375c2..00000000 --- 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 49d45fc2..00000000 --- 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_throttling.py b/rest_framework/tests/test_throttling.py deleted file mode 100644 index 19bc691a..00000000 --- a/rest_framework/tests/test_throttling.py +++ /dev/null @@ -1,246 +0,0 @@ -""" -Tests for the throttling implementations in the permissions module. -""" -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.test import APIRequestFactory -from rest_framework.views import APIView -from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle -from rest_framework.response import Response - - -class User3SecRateThrottle(UserRateThrottle): - rate = '3/sec' - scope = 'seconds' - - -class User3MinRateThrottle(UserRateThrottle): - rate = '3/min' - scope = 'minutes' - - -class MockView(APIView): - throttle_classes = (User3SecRateThrottle,) - - def get(self, request): - return Response('foo') - - -class MockView_MinuteThrottling(APIView): - throttle_classes = (User3MinRateThrottle,) - - def get(self, request): - return Response('foo') - - -class ThrottlingTests(TestCase): - def setUp(self): - """ - Reset the cache so that no throttles will be active - """ - cache.clear() - self.factory = APIRequestFactory() - - def test_requests_are_throttled(self): - """ - Ensure request rate is limited - """ - request = self.factory.get('/') - for dummy in range(4): - response = MockView.as_view()(request) - self.assertEqual(429, response.status_code) - - def set_throttle_timer(self, view, value): - """ - Explicitly set the timer, overriding time.time() - """ - view.throttle_classes[0].timer = lambda self: value - - def test_request_throttling_expires(self): - """ - Ensure request rate is limited for a limited duration only - """ - self.set_throttle_timer(MockView, 0) - - request = self.factory.get('/') - for dummy in range(4): - response = MockView.as_view()(request) - self.assertEqual(429, response.status_code) - - # Advance the timer by one second - self.set_throttle_timer(MockView, 1) - - response = MockView.as_view()(request) - self.assertEqual(200, response.status_code) - - def ensure_is_throttled(self, view, expect): - request = self.factory.get('/') - request.user = User.objects.create(username='a') - for dummy in range(3): - view.as_view()(request) - request.user = User.objects.create(username='b') - response = view.as_view()(request) - self.assertEqual(expect, response.status_code) - - def test_request_throttling_is_per_user(self): - """ - Ensure request rate is only limited per user, not globally for - PerUserThrottles - """ - self.ensure_is_throttled(MockView, 200) - - 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 - set properly. - """ - request = self.factory.get('/') - for timer, expect in expected_headers: - self.set_throttle_timer(view, timer) - response = view.as_view()(request) - if expect is not None: - self.assertEqual(response['X-Throttle-Wait-Seconds'], expect) - else: - self.assertFalse('X-Throttle-Wait-Seconds' 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') - )) - - 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') - )) - - 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) - )) - - -class ScopedRateThrottleTests(TestCase): - """ - Tests for ScopedRateThrottle. - """ - - def setUp(self): - class XYScopedRateThrottle(ScopedRateThrottle): - TIMER_SECONDS = 0 - THROTTLE_RATES = {'x': '3/min', 'y': '1/min'} - timer = lambda self: self.TIMER_SECONDS - - class XView(APIView): - throttle_classes = (XYScopedRateThrottle,) - throttle_scope = 'x' - - def get(self, request): - return Response('x') - - class YView(APIView): - throttle_classes = (XYScopedRateThrottle,) - throttle_scope = 'y' - - def get(self, request): - return Response('y') - - class UnscopedView(APIView): - throttle_classes = (XYScopedRateThrottle,) - - def get(self, request): - return Response('y') - - self.throttle_class = XYScopedRateThrottle - self.factory = APIRequestFactory() - self.x_view = XView.as_view() - self.y_view = YView.as_view() - self.unscoped_view = UnscopedView.as_view() - - def increment_timer(self, seconds=1): - self.throttle_class.TIMER_SECONDS += seconds - - def test_scoped_rate_throttle(self): - request = self.factory.get('/') - - # Should be able to hit x view 3 times per minute. - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(429, response.status_code) - - # Should be able to hit y view 1 time per minute. - self.increment_timer() - response = self.y_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.y_view(request) - self.assertEqual(429, response.status_code) - - # Ensure throttles properly reset by advancing the rest of the minute - self.increment_timer(55) - - # Should still be able to hit x view 3 times per minute. - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.x_view(request) - self.assertEqual(429, response.status_code) - - # Should still be able to hit y view 1 time per minute. - self.increment_timer() - response = self.y_view(request) - self.assertEqual(200, response.status_code) - - self.increment_timer() - response = self.y_view(request) - self.assertEqual(429, response.status_code) - - def test_unscoped_view_not_throttled(self): - request = self.factory.get('/') - - for idx in range(10): - self.increment_timer() - response = self.unscoped_view(request) - self.assertEqual(200, response.status_code) diff --git a/rest_framework/tests/test_urlpatterns.py b/rest_framework/tests/test_urlpatterns.py deleted file mode 100644 index 8132ec4c..00000000 --- a/rest_framework/tests/test_urlpatterns.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import unicode_literals -from collections import namedtuple -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 - - -# A container class for test paths for the test case -URLTestPath = namedtuple('URLTestPath', ['path', 'args', 'kwargs']) - - -def dummy_view(request, *args, **kwargs): - pass - - -class FormatSuffixTests(TestCase): - """ - Tests `format_suffix_patterns` against different URLPatterns to ensure the URLs still resolve properly, including any captured parameters. - """ - def _resolve_urlpatterns(self, urlpatterns, test_paths): - factory = APIRequestFactory() - try: - urlpatterns = format_suffix_patterns(urlpatterns) - except Exception: - self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") - resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) - for test_path in test_paths: - request = factory.get(test_path.path) - try: - callback, callback_args, callback_kwargs = resolver.resolve(request.path_info) - except Exception: - self.fail("Failed to resolve URL: %s" % request.path_info) - self.assertEqual(callback_args, test_path.args) - self.assertEqual(callback_kwargs, test_path.kwargs) - - def test_format_suffix(self): - urlpatterns = patterns( - '', - url(r'^test$', dummy_view), - ) - test_paths = [ - URLTestPath('/test', (), {}), - URLTestPath('/test.api', (), {'format': 'api'}), - URLTestPath('/test.asdf', (), {'format': 'asdf'}), - ] - self._resolve_urlpatterns(urlpatterns, test_paths) - - def test_default_args(self): - urlpatterns = patterns( - '', - url(r'^test$', dummy_view, {'foo': 'bar'}), - ) - test_paths = [ - URLTestPath('/test', (), {'foo': 'bar', }), - URLTestPath('/test.api', (), {'foo': 'bar', 'format': 'api'}), - URLTestPath('/test.asdf', (), {'foo': 'bar', 'format': 'asdf'}), - ] - self._resolve_urlpatterns(urlpatterns, test_paths) - - def test_included_urls(self): - nested_patterns = patterns( - '', - url(r'^path$', dummy_view) - ) - urlpatterns = patterns( - '', - url(r'^test/', include(nested_patterns), {'foo': 'bar'}), - ) - test_paths = [ - URLTestPath('/test/path', (), {'foo': 'bar', }), - URLTestPath('/test/path.api', (), {'foo': 'bar', 'format': 'api'}), - URLTestPath('/test/path.asdf', (), {'foo': 'bar', 'format': 'asdf'}), - ] - self._resolve_urlpatterns(urlpatterns, test_paths) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py deleted file mode 100644 index ebfdff9c..00000000 --- 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_<field> 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/test_views.py b/rest_framework/tests/test_views.py deleted file mode 100644 index c0bec5ae..00000000 --- a/rest_framework/tests/test_views.py +++ /dev/null @@ -1,101 +0,0 @@ -from __future__ import unicode_literals - -import copy -from django.test import TestCase -from rest_framework import status -from rest_framework.decorators import api_view -from rest_framework.response import Response -from rest_framework.settings import api_settings -from rest_framework.test import APIRequestFactory -from rest_framework.views import APIView - -factory = APIRequestFactory() - - -class BasicView(APIView): - def get(self, request, *args, **kwargs): - return Response({'method': 'GET'}) - - def post(self, request, *args, **kwargs): - return Response({'method': 'POST', 'data': request.DATA}) - - -@api_view(['GET', 'POST', 'PUT', 'PATCH']) -def basic_view(request): - if request.method == 'GET': - return {'method': 'GET'} - elif request.method == 'POST': - return {'method': 'POST', 'data': request.DATA} - elif request.method == 'PUT': - return {'method': 'PUT', 'data': request.DATA} - elif request.method == 'PATCH': - return {'method': 'PATCH', 'data': request.DATA} - - -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') - ret['detail'] = ret['detail'][:chop] - return ret - - -class ClassBasedViewIntegrationTests(TestCase): - def setUp(self): - self.view = BasicView.as_view() - - def test_400_parse_error(self): - request = factory.post('/', 'f00bar', content_type='application/json') - response = self.view(request) - expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' - } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) - - def test_400_parse_error_tunneled_content(self): - content = 'f00bar' - content_type = 'application/json' - form_data = { - api_settings.FORM_CONTENT_OVERRIDE: content, - api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type - } - request = factory.post('/', form_data) - response = self.view(request) - expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' - } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) - - -class FunctionBasedViewIntegrationTests(TestCase): - def setUp(self): - self.view = basic_view - - def test_400_parse_error(self): - request = factory.post('/', 'f00bar', content_type='application/json') - response = self.view(request) - expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' - } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) - - def test_400_parse_error_tunneled_content(self): - content = 'f00bar' - content_type = 'application/json' - form_data = { - api_settings.FORM_CONTENT_OVERRIDE: content, - api_settings.FORM_CONTENTTYPE_OVERRIDE: content_type - } - request = factory.post('/', form_data) - response = self.view(request) - expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' - } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py deleted file mode 100644 index 554ebd1a..00000000 --- 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 f6bb1cc8..261fc246 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 d9143bb4..038e9ee3 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 9c4719f1..cfcee534 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 d51374b0..e6690d17 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 b26a2085..2160d18b 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 00000000..c97ec5d0 --- /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 4bec8387..8b6f005e 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', '<br />') + description = '<p>' + description + '</p>' return mark_safe(description) diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py new file mode 100644 index 00000000..d773952d --- /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 00000000..649f2abc --- /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 c09c2933..de2931c2 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 00000000..d92bceb9 --- /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 00000000..1bfc64c1 --- /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 + # <django.core.validators.RegexValidator object at 0x1047af050> + # Should be presented as + # <django.core.validators.RegexValidator object> + 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 00000000..87bb3ac0 --- /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 00000000..880ef9ed --- /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 00000000..6ae80b89 --- /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 `UniqueFor<Range>Validator` 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 00000000..51b886f3 --- /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<version>{v1,v2})/users/$', users_list, name='users-list'), + url(r'^(?P<version>{v1,v2})/users/(?P<pk>[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<pk>[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 37bba7f0..b4abc4d9 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 - - # Note: session based authentication is explicitly CSRF validated, - # all other authentication is CSRF exempt. - @csrf_exempt + exception_handler = self.settings.EXCEPTION_HANDLER + + 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 d91323f2..88c763da 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. |
