From 7224b20d58ceee22abc987980ab646ab8cb2d8dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Jun 2013 17:17:39 +0100 Subject: [PATCH 01/48] Added APIRequestFactory --- rest_framework/compat.py | 38 +++++++++++- rest_framework/renderers.py | 11 ++++ rest_framework/response.py | 2 +- rest_framework/test.py | 48 +++++++++++++++ rest_framework/tests/test_authentication.py | 6 +- rest_framework/tests/test_decorators.py | 11 ++-- rest_framework/tests/test_filters.py | 4 +- rest_framework/tests/test_generics.py | 60 ++++++++----------- .../tests/test_hyperlinkedserializers.py | 11 ++-- rest_framework/tests/test_negotiation.py | 4 +- rest_framework/tests/test_pagination.py | 6 +- rest_framework/tests/test_permissions.py | 35 +++++------ .../tests/test_relations_hyperlink.py | 4 +- rest_framework/tests/test_renderers.py | 8 +-- rest_framework/tests/test_request.py | 15 +---- rest_framework/tests/test_reverse.py | 4 +- rest_framework/tests/test_routers.py | 5 +- rest_framework/tests/test_throttling.py | 6 +- rest_framework/tests/test_urlpatterns.py | 4 +- rest_framework/tests/test_validation.py | 8 +-- rest_framework/tests/test_views.py | 6 +- 21 files changed, 180 insertions(+), 116 deletions(-) create mode 100644 rest_framework/test.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index cb1228465..6f7447add 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -8,6 +8,7 @@ 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`. try: @@ -83,7 +84,6 @@ def get_concrete_model(model_cls): # Django 1.5 add support for custom auth user model if django.VERSION >= (1, 5): - from django.conf import settings AUTH_USER_MODEL = settings.AUTH_USER_MODEL else: AUTH_USER_MODEL = 'auth.User' @@ -436,6 +436,42 @@ except ImportError: return force_text(url) +# RequestFactory only provide `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 +except ImportError: + # In 1.3 and 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) + 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), + } + 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('') + }) + r.update(extra) + return self.request(**r) + # Markdown is optional try: import markdown diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8b2428ad8..d7a7ef297 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -14,6 +14,7 @@ from django import forms from django.core.exceptions import ImproperlyConfigured from django.http.multipartparser import parse_header from django.template import 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 @@ -571,3 +572,13 @@ class BrowsableAPIRenderer(BaseRenderer): response.status_code = status.HTTP_200_OK return ret + + +class MultiPartRenderer(BaseRenderer): + media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' + format = 'form' + charset = 'utf-8' + BOUNDARY = 'BoUnDaRyStRiNg' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return encode_multipart(self.BOUNDARY, data) diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3e..c4b2aaa66 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -50,7 +50,7 @@ class Response(SimpleTemplateResponse): charset = renderer.charset content_type = self.content_type - if content_type is None and charset is not None: + if content_type is None and charset is not None and ';' not in media_type: content_type = "{0}; charset={1}".format(media_type, charset) elif content_type is None: content_type = media_type diff --git a/rest_framework/test.py b/rest_framework/test.py new file mode 100644 index 000000000..92281cafc --- /dev/null +++ b/rest_framework/test.py @@ -0,0 +1,48 @@ +from rest_framework.compat import six, RequestFactory +from rest_framework.renderers import JSONRenderer, MultiPartRenderer + + +class APIRequestFactory(RequestFactory): + renderer_classes = { + 'json': JSONRenderer, + 'form': MultiPartRenderer + } + default_format = 'form' + + def __init__(self, format=None, **defaults): + self.format = format or self.default_format + super(APIRequestFactory, self).__init__(**defaults) + + def _encode_data(self, data, format, content_type): + if not data: + return ('', None) + + format = format or self.format + + if content_type is None and data is not None: + renderer = self.renderer_classes[format]() + data = renderer.render(data) + # Determine the content-type header + if ';' in renderer.media_type: + content_type = renderer.media_type + else: + content_type = "{0}; charset={1}".format( + renderer.media_type, renderer.charset + ) + # Coerce text to bytes if required. + if isinstance(data, six.text_type): + data = bytes(data.encode(renderer.charset)) + + return data, content_type + + 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) + + def put(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('PUT', path, data, content_type, **extra) + + def patch(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('PATCH', path, data, content_type, **extra) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 6a50be064..f2c51c68f 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -21,14 +21,14 @@ 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.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.views import APIView -import json import base64 import time import datetime +import json -factory = RequestFactory() +factory = APIRequestFactory() class MockView(APIView): diff --git a/rest_framework/tests/test_decorators.py b/rest_framework/tests/test_decorators.py index 1016fed3f..195f0ba3e 100644 --- a/rest_framework/tests/test_decorators.py +++ b/rest_framework/tests/test_decorators.py @@ -1,12 +1,13 @@ 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.parsers import JSONParser -from rest_framework.authentication import BasicAuthentication +from rest_framework.test import APIRequestFactory from rest_framework.throttling import UserRateThrottle -from rest_framework.permissions import IsAuthenticated from rest_framework.views import APIView from rest_framework.decorators import ( api_view, @@ -17,13 +18,11 @@ from rest_framework.decorators import ( permission_classes, ) -from rest_framework.tests.utils import RequestFactory - class DecoratorTestCase(TestCase): def setUp(self): - self.factory = RequestFactory() + self.factory = APIRequestFactory() def _finalize_response(self, request, response, *args, **kwargs): response.request = request diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index aaed62478..c9d9e7ffa 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -4,13 +4,13 @@ from decimal import Decimal from django.db import models from django.core.urlresolvers import reverse from django.test import TestCase -from django.test.client import RequestFactory 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 = RequestFactory() +factory = APIRequestFactory() class FilterableItem(models.Model): diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 37734195a..1550880b5 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -3,12 +3,11 @@ 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.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel from rest_framework.compat import six -import json -factory = RequestFactory() +factory = APIRequestFactory() class RootView(generics.ListCreateAPIView): @@ -71,9 +70,8 @@ class TestRootView(TestCase): """ POST requests to ListCreateAPIView should create a new object. """ - content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + 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) @@ -85,9 +83,8 @@ class TestRootView(TestCase): """ PUT requests to ListCreateAPIView should not be allowed """ - content = {'text': 'foobar'} - request = factory.put('/', json.dumps(content), - content_type='application/json') + 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) @@ -148,9 +145,8 @@ class TestRootView(TestCase): """ POST requests to create a new object should not be able to set the id. """ - content = {'id': 999, 'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + 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) @@ -189,9 +185,8 @@ class TestInstanceView(TestCase): """ POST requests to RetrieveUpdateDestroyAPIView should not be allowed """ - content = {'text': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + 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) @@ -201,9 +196,8 @@ class TestInstanceView(TestCase): """ PUT requests to RetrieveUpdateDestroyAPIView should update an object. """ - content = {'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + 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) @@ -215,9 +209,8 @@ class TestInstanceView(TestCase): """ PATCH requests to RetrieveUpdateDestroyAPIView should update an object. """ - content = {'text': 'foobar'} - request = factory.patch('/1', json.dumps(content), - content_type='application/json') + data = {'text': 'foobar'} + request = factory.patch('/1', data, format='json') with self.assertNumQueries(2): response = self.view(request, pk=1).render() @@ -293,9 +286,8 @@ class TestInstanceView(TestCase): """ PUT requests to create a new object should not be able to set the id. """ - content = {'id': 999, 'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + 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) @@ -309,9 +301,8 @@ class TestInstanceView(TestCase): if it does not currently exist. """ self.objects.get(id=1).delete() - content = {'text': 'foobar'} - request = factory.put('/1', json.dumps(content), - content_type='application/json') + 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) @@ -324,10 +315,9 @@ class TestInstanceView(TestCase): PUT requests to RetrieveUpdateDestroyAPIView should create an object at the requested url if it doesn't exist. """ - content = {'text': 'foobar'} + 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', json.dumps(content), - content_type='application/json') + 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) @@ -339,9 +329,8 @@ class TestInstanceView(TestCase): PUT requests to RetrieveUpdateDestroyAPIView should create an object at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. """ - content = {'text': 'foobar'} - request = factory.put('/test_slug', json.dumps(content), - content_type='application/json') + 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) @@ -415,9 +404,8 @@ class TestCreateModelWithAutoNowAddField(TestCase): https://github.com/tomchristie/django-rest-framework/issues/285 """ - content = {'email': 'foobar@example.com', 'content': 'foobar'} - request = factory.post('/', json.dumps(content), - content_type='application/json') + 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) diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py index 129600cb4..61e613d75 100644 --- a/rest_framework/tests/test_hyperlinkedserializers.py +++ b/rest_framework/tests/test_hyperlinkedserializers.py @@ -1,12 +1,15 @@ from __future__ import unicode_literals import json from django.test import TestCase -from django.test.client import RequestFactory from rest_framework import generics, status, serializers from rest_framework.compat import patterns, url -from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel +from rest_framework.test import APIRequestFactory +from rest_framework.tests.models import ( + Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, + Album, Photo, OptionalRelationModel +) -factory = RequestFactory() +factory = APIRequestFactory() class BlogPostCommentSerializer(serializers.ModelSerializer): @@ -21,7 +24,7 @@ class BlogPostCommentSerializer(serializers.ModelSerializer): class PhotoSerializer(serializers.Serializer): description = serializers.CharField() - album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), slug_field='title', slug_url_kwarg='title') + 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) diff --git a/rest_framework/tests/test_negotiation.py b/rest_framework/tests/test_negotiation.py index 7f84827f0..04b89eb60 100644 --- a/rest_framework/tests/test_negotiation.py +++ b/rest_framework/tests/test_negotiation.py @@ -1,12 +1,12 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory 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 = RequestFactory() +factory = APIRequestFactory() class MockJSONRenderer(BaseRenderer): diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index e538a78e5..85d4640ea 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -4,13 +4,13 @@ from decimal import Decimal from django.db import models from django.core.paginator import Paginator from django.test import TestCase -from django.test.client import RequestFactory 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 = RequestFactory() +factory = APIRequestFactory() class FilterableItem(models.Model): @@ -369,7 +369,7 @@ class TestCustomPaginationSerializer(TestCase): self.page = paginator.page(1) def test_custom_pagination_serializer(self): - request = RequestFactory().get('/foobar') + request = APIRequestFactory().get('/foobar') serializer = CustomPaginationSerializer( instance=self.page, context={'request': request} diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index 6caaf65b0..e2cca3808 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -3,11 +3,10 @@ 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.tests.utils import RequestFactory +from rest_framework.test import APIRequestFactory import base64 -import json -factory = RequestFactory() +factory = APIRequestFactory() class BasicModel(models.Model): @@ -56,15 +55,13 @@ class ModelPermissionsIntegrationTests(TestCase): BasicModel(text='foo').save() def test_has_create_permissions(self): - request = factory.post('/', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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) @@ -75,15 +72,13 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) def test_does_not_have_create_permissions(self): - request = factory.post('/', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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) @@ -95,28 +90,26 @@ class ModelPermissionsIntegrationTests(TestCase): def test_has_put_as_create_permissions(self): # User only has update permissions - should be able to update an entity. - request = factory.put('/1', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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', json.dumps({'text': 'foobar'}), - content_type='application/json', + 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('/', content_type='application/json', + 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', content_type='application/json', + request = factory.options('/1', HTTP_AUTHORIZATION=self.permitted_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -124,26 +117,26 @@ class ModelPermissionsIntegrationTests(TestCase): self.assertEqual(list(response.data['actions'].keys()), ['PUT']) def test_options_disallowed(self): - request = factory.options('/', content_type='application/json', + 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', content_type='application/json', + 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('/', content_type='application/json', + 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', content_type='application/json', + request = factory.options('/1', HTTP_AUTHORIZATION=self.updateonly_credentials) response = instance_view(request, pk='1') self.assertEqual(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/tests/test_relations_hyperlink.py b/rest_framework/tests/test_relations_hyperlink.py index 2ca7f4f2b..3c4d39af6 100644 --- a/rest_framework/tests/test_relations_hyperlink.py +++ b/rest_framework/tests/test_relations_hyperlink.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory 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 = RequestFactory() +factory = APIRequestFactory() request = factory.get('/') # Just to ensure we have a request in the serializer context diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 95b597411..df6f4aa63 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -4,19 +4,17 @@ from __future__ import unicode_literals from decimal import Decimal from django.core.cache import cache from django.test import TestCase -from django.test.client import RequestFactory 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 +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.compat import StringIO -from rest_framework.compat import six +from rest_framework.test import APIRequestFactory import datetime import pickle import re @@ -121,7 +119,7 @@ class POSTDeniedView(APIView): class DocumentingRendererTests(TestCase): def test_only_permitted_forms_are_displayed(self): view = POSTDeniedView.as_view() - request = RequestFactory().get('/') + request = APIRequestFactory().get('/') response = view(request).render() self.assertNotContains(response, '>POST<') self.assertContains(response, '>PUT<') diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index a5c5e84ce..8d64d79f2 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -6,7 +6,6 @@ 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, Client -from django.test.client import RequestFactory from rest_framework import status from rest_framework.authentication import SessionAuthentication from rest_framework.compat import patterns @@ -19,12 +18,13 @@ from rest_framework.parsers import ( 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 from rest_framework.views import APIView from rest_framework.compat import six import json -factory = RequestFactory() +factory = APIRequestFactory() class PlainTextParser(BaseParser): @@ -116,16 +116,7 @@ class TestContentParsing(TestCase): Ensure request.DATA returns content for PUT request with form content. """ data = {'qwerty': 'uiop'} - - from django import VERSION - - if VERSION >= (1, 5): - from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart - request = Request(factory.put('/', encode_multipart(BOUNDARY, data), - content_type=MULTIPART_CONTENT)) - else: - request = Request(factory.put('/', data)) - + request = Request(factory.put('/', data)) request.parsers = (FormParser(), MultiPartParser()) self.assertEqual(list(request.DATA.items()), list(data.items())) diff --git a/rest_framework/tests/test_reverse.py b/rest_framework/tests/test_reverse.py index 93ef56377..690a30b11 100644 --- a/rest_framework/tests/test_reverse.py +++ b/rest_framework/tests/test_reverse.py @@ -1,10 +1,10 @@ from __future__ import unicode_literals from django.test import TestCase -from django.test.client import RequestFactory from rest_framework.compat import patterns, url from rest_framework.reverse import reverse +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() def null_view(request): diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py index d375f4a8c..5fcccb741 100644 --- a/rest_framework/tests/test_routers.py +++ b/rest_framework/tests/test_routers.py @@ -1,15 +1,15 @@ from __future__ import unicode_literals from django.db import models from django.test import TestCase -from django.test.client import RequestFactory 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 = RequestFactory() +factory = APIRequestFactory() urlpatterns = patterns('',) @@ -193,6 +193,7 @@ class TestActionKeywordArgs(TestCase): {'permission_classes': [permissions.AllowAny]} ) + class TestActionAppliedToExistingRoute(TestCase): """ Ensure `@action` decorator raises an except when applied diff --git a/rest_framework/tests/test_throttling.py b/rest_framework/tests/test_throttling.py index d35d37092..19bc691ae 100644 --- a/rest_framework/tests/test_throttling.py +++ b/rest_framework/tests/test_throttling.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals from django.test import TestCase from django.contrib.auth.models import User from django.core.cache import cache -from django.test.client import RequestFactory +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 @@ -41,7 +41,7 @@ class ThrottlingTests(TestCase): Reset the cache so that no throttles will be active """ cache.clear() - self.factory = RequestFactory() + self.factory = APIRequestFactory() def test_requests_are_throttled(self): """ @@ -173,7 +173,7 @@ class ScopedRateThrottleTests(TestCase): return Response('y') self.throttle_class = XYScopedRateThrottle - self.factory = RequestFactory() + self.factory = APIRequestFactory() self.x_view = XView.as_view() self.y_view = YView.as_view() self.unscoped_view = UnscopedView.as_view() diff --git a/rest_framework/tests/test_urlpatterns.py b/rest_framework/tests/test_urlpatterns.py index 29ed4a961..8132ec4c8 100644 --- a/rest_framework/tests/test_urlpatterns.py +++ b/rest_framework/tests/test_urlpatterns.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from collections import namedtuple from django.core import urlresolvers from django.test import TestCase -from django.test.client import RequestFactory +from rest_framework.test import APIRequestFactory from rest_framework.compat import patterns, url, include from rest_framework.urlpatterns import format_suffix_patterns @@ -20,7 +20,7 @@ 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 = RequestFactory() + factory = APIRequestFactory() try: urlpatterns = format_suffix_patterns(urlpatterns) except Exception: diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index a6ec0e993..ebfdff9cd 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -2,10 +2,9 @@ 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.tests.utils import RequestFactory -import json +from rest_framework.test import APIRequestFactory -factory = RequestFactory() +factory = APIRequestFactory() # Regression for #666 @@ -33,8 +32,7 @@ class TestPreSaveValidationExclusions(TestCase): validation on read only fields. """ obj = ValidationModel.objects.create(blank_validated_field='') - request = factory.put('/', json.dumps({}), - content_type='application/json') + 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) diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py index 2767d24c8..c0bec5aed 100644 --- a/rest_framework/tests/test_views.py +++ b/rest_framework/tests/test_views.py @@ -1,17 +1,15 @@ from __future__ import unicode_literals import copy - from django.test import TestCase -from django.test.client import RequestFactory - 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 = RequestFactory() +factory = APIRequestFactory() class BasicView(APIView): From f585480ee10f4b5e61db4ac343b1d2af25d2de97 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Jun 2013 17:50:30 +0100 Subject: [PATCH 02/48] Added APIClient --- rest_framework/test.py | 83 +++++++++++++++++---- rest_framework/tests/test_authentication.py | 44 +++++------ rest_framework/tests/test_request.py | 6 +- 3 files changed, 94 insertions(+), 39 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 92281cafc..9fce2c08b 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,39 +1,54 @@ -from rest_framework.compat import six, RequestFactory +# Note that we use `DjangoRequestFactory` and `DjangoClient` names in order +# to make it harder for the user to import the wrong thing without realizing. +from django.conf import settings +from django.test.client import Client as DjangoClient +from rest_framework.compat import RequestFactory as DjangoRequestFactory +from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.renderers import JSONRenderer, MultiPartRenderer -class APIRequestFactory(RequestFactory): +class APIRequestFactory(DjangoRequestFactory): renderer_classes = { 'json': JSONRenderer, 'form': MultiPartRenderer } default_format = 'form' - def __init__(self, format=None, **defaults): - self.format = format or self.default_format - super(APIRequestFactory, self).__init__(**defaults) + def _encode_data(self, data, format=None, content_type=None): + """ + Encode the data returning a two tuple of (bytes, content_type) + """ - def _encode_data(self, data, format, content_type): if not data: return ('', None) - format = format or self.format + assert format is None or content_type is None, ( + 'You may not set both `format` and `content_type`.' + ) - if content_type is None and data is not None: + if content_type: + # Content type specified explicitly, treat data as a raw bytestring + ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) + + else: + # Use format and render the data into a bytestring + format = format or self.default_format renderer = self.renderer_classes[format]() - data = renderer.render(data) - # Determine the content-type header + ret = renderer.render(data) + + # Determine the content-type header from the renderer if ';' in renderer.media_type: content_type = renderer.media_type else: content_type = "{0}; charset={1}".format( renderer.media_type, renderer.charset ) - # Coerce text to bytes if required. - if isinstance(data, six.text_type): - data = bytes(data.encode(renderer.charset)) - return data, content_type + # Coerce text to bytes if required. + if isinstance(ret, six.text_type): + ret = bytes(ret.encode(renderer.charset)) + + return ret, content_type def post(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) @@ -46,3 +61,43 @@ class APIRequestFactory(RequestFactory): def patch(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) return self.generic('PATCH', path, data, content_type, **extra) + + def delete(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('DELETE', path, data, content_type, **extra) + + def options(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('OPTIONS', path, data, content_type, **extra) + + +class APIClient(APIRequestFactory, DjangoClient): + 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).post(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).post(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).post(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).post(path, data=data, format=format, content_type=content_type, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index f2c51c68f..a44813b69 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.http import HttpResponse -from django.test import Client, TestCase +from django.test import TestCase from django.utils import unittest from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions @@ -21,12 +21,11 @@ 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 +from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView import base64 import time import datetime -import json factory = APIRequestFactory() @@ -68,7 +67,7 @@ class BasicAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -87,7 +86,7 @@ class BasicAuthTests(TestCase): 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/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) + 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): @@ -97,7 +96,7 @@ class BasicAuthTests(TestCase): def test_post_json_failing_basic_auth(self): """Ensure POSTing json over basic auth without correct credentials fails""" - response = self.csrf_client.post('/basic/', json.dumps({'example': 'example'}), 'application/json') + 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"') @@ -107,8 +106,8 @@ class SessionAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) - self.non_csrf_client = Client(enforce_csrf_checks=False) + 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' @@ -154,7 +153,7 @@ class TokenAuthTests(TestCase): urls = 'rest_framework.tests.test_authentication' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -172,7 +171,7 @@ class TokenAuthTests(TestCase): 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/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth) + 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): @@ -182,7 +181,7 @@ class TokenAuthTests(TestCase): def test_post_json_failing_token_auth(self): """Ensure POSTing json over token auth without correct credentials fails""" - response = self.csrf_client.post('/token/', json.dumps({'example': 'example'}), 'application/json') + 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): @@ -193,33 +192,33 @@ class TokenAuthTests(TestCase): def test_token_login_json(self): """Ensure token login view using JSON POST works.""" - client = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username, 'password': self.password}), 'application/json') + {'username': self.username, 'password': self.password}, format='json') self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(json.loads(response.content.decode('ascii'))['token'], self.key) + 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 = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username, 'password': "badpass"}), 'application/json') + {'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 = Client(enforce_csrf_checks=True) + client = APIClient(enforce_csrf_checks=True) response = client.post('/auth-token/', - json.dumps({'username': self.username}), 'application/json') + {'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 = Client(enforce_csrf_checks=True) + 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(json.loads(response.content.decode('ascii'))['token'], self.key) + self.assertEqual(response.data['token'], self.key) class IncorrectCredentialsTests(TestCase): @@ -256,7 +255,7 @@ class OAuthTests(TestCase): self.consts = consts - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' @@ -470,12 +469,13 @@ class OAuthTests(TestCase): 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 = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 8d64d79f2..969d8024a 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -5,7 +5,7 @@ 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, Client +from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication from rest_framework.compat import patterns @@ -18,7 +18,7 @@ from rest_framework.parsers import ( 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 +from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework.compat import six import json @@ -248,7 +248,7 @@ class TestContentParsingWithAuthentication(TestCase): urls = 'rest_framework.tests.test_request' def setUp(self): - self.csrf_client = Client(enforce_csrf_checks=True) + self.csrf_client = APIClient(enforce_csrf_checks=True) self.username = 'john' self.email = 'lennon@thebeatles.com' self.password = 'password' From 90bc07f3f160485001ea329e5f69f7e521d14ec9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:05:08 +0100 Subject: [PATCH 03/48] Addeded 'APITestClient.credentials()' --- rest_framework/test.py | 29 +++++++++++++++++++++++++ rest_framework/tests/test_testing.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 rest_framework/tests/test_testing.py diff --git a/rest_framework/test.py b/rest_framework/test.py index 9fce2c08b..8115fa0d2 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,5 +1,8 @@ +# -- coding: utf-8 -- + # Note that we use `DjangoRequestFactory` and `DjangoClient` names in order # to make it harder for the user to import the wrong thing without realizing. +from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient from rest_framework.compat import RequestFactory as DjangoRequestFactory @@ -72,31 +75,57 @@ class APIRequestFactory(DjangoRequestFactory): class APIClient(APIRequestFactory, DjangoClient): + def __init__(self, *args, **kwargs): + self._credentials = {} + super(APIClient, self).__init__(*args, **kwargs) + + def credentials(self, **kwargs): + self._credentials = kwargs + + def get(self, path, data={}, follow=False, **extra): + extra.update(self._credentials) + response = super(APIClient, self).get(path, data=data, **extra) + if follow: + response = self._handle_redirects(response, **extra) + return response + + def head(self, path, data={}, follow=False, **extra): + extra.update(self._credentials) + response = super(APIClient, self).head(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): + extra.update(self._credentials) 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): + extra.update(self._credentials) 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 patch(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) 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 delete(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) 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 options(self, path, data=None, format=None, content_type=None, follow=False, **extra): + extra.update(self._credentials) response = super(APIClient, self).post(path, data=data, format=format, content_type=content_type, **extra) if follow: response = self._handle_redirects(response, **extra) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py new file mode 100644 index 000000000..71dacd38e --- /dev/null +++ b/rest_framework/tests/test_testing.py @@ -0,0 +1,32 @@ +# -- coding: utf-8 -- + +from __future__ import unicode_literals +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 + + +@api_view(['GET']) +def mirror(request): + return Response({ + 'auth': request.META.get('HTTP_AUTHORIZATION', b'') + }) + + +urlpatterns = patterns('', + url(r'^view/$', mirror), +) + + +class CheckTestClient(TestCase): + urls = 'rest_framework.tests.test_testing' + + def setUp(self): + self.client = APIClient() + + def test_credentials(self): + self.client.credentials(HTTP_AUTHORIZATION='example') + response = self.client.get('/view/') + self.assertEqual(response.data['auth'], 'example') From f7db06953bd8ad7f5e0211f49a04e8d5bb634380 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:06:11 +0100 Subject: [PATCH 04/48] Remove unneeded tests.utils, superseeded by APIRequestFactory, APIClient --- rest_framework/tests/utils.py | 40 ----------------------------------- 1 file changed, 40 deletions(-) delete mode 100644 rest_framework/tests/utils.py diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py deleted file mode 100644 index 8c87917d9..000000000 --- a/rest_framework/tests/utils.py +++ /dev/null @@ -1,40 +0,0 @@ -from __future__ import unicode_literals -from django.test.client import FakePayload, Client as _Client, RequestFactory as _RequestFactory -from django.test.client import MULTIPART_CONTENT -from rest_framework.compat import urlparse - - -class RequestFactory(_RequestFactory): - - def __init__(self, **defaults): - super(RequestFactory, self).__init__(**defaults) - - def patch(self, path, data={}, content_type=MULTIPART_CONTENT, - **extra): - "Construct a PATCH request." - - patch_data = self._encode_data(data, content_type) - - parsed = urlparse.urlparse(path) - r = { - 'CONTENT_LENGTH': len(patch_data), - 'CONTENT_TYPE': content_type, - 'PATH_INFO': self._get_path(parsed), - 'QUERY_STRING': parsed[4], - 'REQUEST_METHOD': 'PATCH', - 'wsgi.input': FakePayload(patch_data), - } - r.update(extra) - return self.request(**r) - - -class Client(_Client, RequestFactory): - def patch(self, path, data={}, content_type=MULTIPART_CONTENT, - follow=False, **extra): - """ - Send a resource to the server using PATCH. - """ - response = super(Client, self).patch(path, data=data, content_type=content_type, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response From 35022ca9213939a2f40c82facffa908a818efe0b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 08:14:05 +0100 Subject: [PATCH 05/48] Refactor SessionAuthentication slightly --- rest_framework/authentication.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 102980271..b42162dd9 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -26,6 +26,12 @@ def get_authorization_header(request): return auth +class CSRFCheck(CsrfViewMiddleware): + def _reject(self, request, reason): + # Return the failure reason instead of an HttpResponse + return reason + + class BaseAuthentication(object): """ All authentication classes should extend BaseAuthentication. @@ -110,20 +116,20 @@ class SessionAuthentication(BaseAuthentication): if not user or not user.is_active: return None - # Enforce CSRF validation for session based authentication. - class CSRFCheck(CsrfViewMiddleware): - def _reject(self, request, reason): - # Return the failure reason instead of an HttpResponse - return reason - - reason = CSRFCheck().process_view(http_request, None, (), {}) - if reason: - # CSRF failed, bail with explicit error message - raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) + self.enforce_csrf(http_request) # CSRF passed with authenticated user return (user, None) + def enforce_csrf(self, request): + """ + Enforce CSRF validation for session based authentication. + """ + reason = CSRFCheck().process_view(request, None, (), {}) + if reason: + # CSRF failed, bail with explicit error message + raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason) + class TokenAuthentication(BaseAuthentication): """ From 664f8c63655770cd90bdbd510b315bcd045b380a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 21:02:58 +0100 Subject: [PATCH 06/48] Added APIClient.authenticate() --- rest_framework/renderers.py | 2 +- rest_framework/request.py | 20 +++++++++++++ rest_framework/test.py | 39 +++++++++++++++++++++++--- rest_framework/tests/test_testing.py | 42 ++++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 8 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index d7a7ef297..3a03ca332 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -576,7 +576,7 @@ class BrowsableAPIRenderer(BaseRenderer): class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' - format = 'form' + format = 'multipart' charset = 'utf-8' BOUNDARY = 'BoUnDaRyStRiNg' diff --git a/rest_framework/request.py b/rest_framework/request.py index 0d88ebc7e..919716f49 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -64,6 +64,20 @@ def clone_request(request, method): return ret +class ForcedAuthentication(object): + """ + This authentication class is used if the test client or request factory + forcibly authenticated the request. + """ + + def __init__(self, force_user, force_token): + self.force_user = force_user + self.force_token = force_token + + def authenticate(self, request): + return (self.force_user, self.force_token) + + class Request(object): """ Wrapper allowing to enhance a standard `HttpRequest` instance. @@ -98,6 +112,12 @@ class Request(object): self.parser_context['request'] = self self.parser_context['encoding'] = request.encoding or settings.DEFAULT_CHARSET + force_user = getattr(request, '_force_auth_user', None) + force_token = getattr(request, '_force_auth_token', None) + if (force_user is not None or force_token is not None): + forced_auth = ForcedAuthentication(force_user, force_token) + self.authenticators = (forced_auth,) + def _default_negotiator(self): return api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS() diff --git a/rest_framework/test.py b/rest_framework/test.py index 8115fa0d2..08de2297b 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient +from django.test.client import ClientHandler from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import force_bytes_or_smart_bytes, six from rest_framework.renderers import JSONRenderer, MultiPartRenderer @@ -13,9 +14,9 @@ from rest_framework.renderers import JSONRenderer, MultiPartRenderer class APIRequestFactory(DjangoRequestFactory): renderer_classes = { 'json': JSONRenderer, - 'form': MultiPartRenderer + 'multipart': MultiPartRenderer } - default_format = 'form' + default_format = 'multipart' def _encode_data(self, data, format=None, content_type=None): """ @@ -74,14 +75,44 @@ class APIRequestFactory(DjangoRequestFactory): return self.generic('OPTIONS', path, data, content_type, **extra) -class APIClient(APIRequestFactory, DjangoClient): +class ForceAuthClientHandler(ClientHandler): + """ + A patched version of ClientHandler that can enforce authentication + on the outgoing requests. + """ + def __init__(self, *args, **kwargs): + self._force_auth_user = None + self._force_auth_token = None + super(ForceAuthClientHandler, self).__init__(*args, **kwargs) + + def force_authenticate(self, user=None, token=None): + self._force_auth_user = user + self._force_auth_token = token + + def get_response(self, request): + # This is the simplest place we can hook into to patch the + # request object. + request._force_auth_user = self._force_auth_user + request._force_auth_token = self._force_auth_token + return super(ForceAuthClientHandler, self).get_response(request) + + +class APIClient(APIRequestFactory, DjangoClient): + def __init__(self, enforce_csrf_checks=False, **defaults): + # Note that our super call skips Client.__init__ + # since we don't need to instantiate a regular ClientHandler + super(DjangoClient, self).__init__(**defaults) + self.handler = ForceAuthClientHandler(enforce_csrf_checks) + self.exc_info = None self._credentials = {} - super(APIClient, self).__init__(*args, **kwargs) def credentials(self, **kwargs): self._credentials = kwargs + def authenticate(self, user=None, token=None): + self.handler.force_authenticate(user, token) + def get(self, path, data={}, follow=False, **extra): extra.update(self._credentials) response = super(APIClient, self).get(path, data=data, **extra) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 71dacd38e..a8398b9a4 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -1,6 +1,7 @@ # -- 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 @@ -8,10 +9,11 @@ from rest_framework.response import Response from rest_framework.test import APIClient -@api_view(['GET']) +@api_view(['GET', 'POST']) def mirror(request): return Response({ - 'auth': request.META.get('HTTP_AUTHORIZATION', b'') + 'auth': request.META.get('HTTP_AUTHORIZATION', b''), + 'user': request.user.username }) @@ -27,6 +29,40 @@ class CheckTestClient(TestCase): 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_authenticate(self): + """ + Setting `.authenticate()` forcibly authenticates each request. + """ + user = User.objects.create_user('example', 'example@example.com') + self.client.authenticate(user) response = self.client.get('/view/') - self.assertEqual(response.data['auth'], 'example') + 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) From ab799ccc3ee473de61ec35c6f745c6952752c522 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 21:34:47 +0100 Subject: [PATCH 07/48] Simplify APIClient implementation --- rest_framework/authentication.py | 6 +-- rest_framework/test.py | 66 +++++++------------------------- 2 files changed, 16 insertions(+), 56 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index b42162dd9..cf001a24d 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -109,14 +109,14 @@ class SessionAuthentication(BaseAuthentication): """ # Get the underlying HttpRequest object - http_request = request._request - user = getattr(http_request, 'user', None) + request = request._request + user = getattr(request, 'user', None) # Unauthenticated, CSRF validation not required if not user or not user.is_active: return None - self.enforce_csrf(http_request) + self.enforce_csrf(request) # CSRF passed with authenticated user return (user, None) diff --git a/rest_framework/test.py b/rest_framework/test.py index 08de2297b..2e9cfe096 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -86,10 +86,6 @@ class ForceAuthClientHandler(ClientHandler): self._force_auth_token = None super(ForceAuthClientHandler, self).__init__(*args, **kwargs) - def force_authenticate(self, user=None, token=None): - self._force_auth_user = user - self._force_auth_token = token - def get_response(self, request): # This is the simplest place we can hook into to patch the # request object. @@ -108,56 +104,20 @@ class APIClient(APIRequestFactory, DjangoClient): self._credentials = {} def credentials(self, **kwargs): + """ + Sets headers that will be used on every outgoing request. + """ self._credentials = kwargs def authenticate(self, user=None, token=None): - self.handler.force_authenticate(user, token) + """ + Forcibly authenticates outgoing requests with the given + user and/or token. + """ + self.handler._force_auth_user = user + self.handler._force_auth_token = token - def get(self, path, data={}, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).get(path, data=data, **extra) - if follow: - response = self._handle_redirects(response, **extra) - return response - - def head(self, path, data={}, follow=False, **extra): - extra.update(self._credentials) - response = super(APIClient, self).head(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): - extra.update(self._credentials) - 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): - extra.update(self._credentials) - 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 patch(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - 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 delete(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - 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 options(self, path, data=None, format=None, content_type=None, follow=False, **extra): - extra.update(self._credentials) - 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 request(self, **request): + # Ensure that any credentials set get added to every request. + request.update(self._credentials) + return super(APIClient, self).request(**request) From c9485c783a555516e41068996258f4c5e383523b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 22:53:15 +0100 Subject: [PATCH 08/48] Rename to force_authenticate --- rest_framework/test.py | 2 +- rest_framework/tests/test_testing.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 2e9cfe096..2f658a56c 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -109,7 +109,7 @@ class APIClient(APIRequestFactory, DjangoClient): """ self._credentials = kwargs - def authenticate(self, user=None, token=None): + def force_authenticate(self, user=None, token=None): """ Forcibly authenticates outgoing requests with the given user and/or token. diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index a8398b9a4..3706f38c2 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -37,12 +37,12 @@ class CheckTestClient(TestCase): response = self.client.get('/view/') self.assertEqual(response.data['auth'], 'example') - def test_authenticate(self): + def test_force_authenticate(self): """ - Setting `.authenticate()` forcibly authenticates each request. + Setting `.force_authenticate()` forcibly authenticates each request. """ user = User.objects.create_user('example', 'example@example.com') - self.client.authenticate(user) + self.client.force_authenticate(user) response = self.client.get('/view/') self.assertEqual(response.data['user'], 'example') From d31d7c18676b6292e8dc688b61913d572eccde91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 29 Jun 2013 22:53:27 +0100 Subject: [PATCH 09/48] First pass at testing docs --- docs/api-guide/testing.md | 97 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 docs/api-guide/testing.md diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md new file mode 100644 index 000000000..f3a092a9d --- /dev/null +++ b/docs/api-guide/testing.md @@ -0,0 +1,97 @@ + + +# Testing + +> Code without tests is broken as designed +> +> — [Jacob Kaplan-Moss][cite] + +REST framework includes a few helper classes that extend Django's existing test framework, and improve support for making API requests. + +# APIRequestFactory + +Extends Django's existing `RequestFactory`. + +**TODO**: Document making requests. Note difference on form PUT requests. Document configuration. + +# APIClient + +Extends Django's existing `Client`. + +### .login(**kwargs) + +The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`. + + # Make all requests in the context of a logged in session. + >>> client = APIClient() + >>> client.login(username='lauren', password='secret') + +To logout, call the `logout` method as usual. + + # Log out + >>> client.logout() + +The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API. + +### .credentials(**kwargs) + +The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. + + # Include an appropriate `Authorization:` header on all requests. + >>> token = Token.objects.get(username='lauren') + >>> client = APIClient() + >>> client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) + +Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments. + + # Stop including any credentials + >>> client.credentials() + +The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes. + +### .force_authenticate(user=None, token=None) + +Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated. + +This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests. + + >>> user = User.objects.get(username='lauren') + >>> client = APIClient() + >>> client.force_authenticate(user=user) + +To unauthenticate subsequant requests, call `force_authenticate` setting the user and/or token to `None`. + + >>> client.force_authenticate(user=None) + +### Making requests + +**TODO**: Document requests similarly to `APIRequestFactory` + +# Testing responses + +### Using request.data + +When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. + +For example, it's easier to inspect `request.data`: + + response = self.client.get('/users/4/') + self.assertEqual(response.data, {'id': 4, 'username': 'lauren'}) + +Instead of inspecting the result of parsing `request.content`: + + response = self.client.get('/users/4/') + self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) + +### Rendering responses + +If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response. + + view = UserDetail.as_view() + request = factory.get('/users/4') + response = view(request, pk='4') + response.render() # Cannot access `response.content` without this. + self.assertEqual(response.content, '{"username": "lauren", "id": 4}') + + +[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper \ No newline at end of file From 0a722de171b0e80ac26d8c77b8051a4170bdb4c6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 13:59:05 +0100 Subject: [PATCH 10/48] Complete testing docs --- docs/api-guide/renderers.md | 11 +++ docs/api-guide/settings.md | 27 +++++ docs/api-guide/testing.md | 143 +++++++++++++++++++++++++-- rest_framework/response.py | 2 +- rest_framework/settings.py | 8 ++ rest_framework/test.py | 70 ++++++++----- rest_framework/tests/test_testing.py | 55 ++++++++++- 7 files changed, 274 insertions(+), 42 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index b627c9306..869bdc16a 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -217,6 +217,16 @@ Renders data into HTML for the Browsable API. This renderer will determine whic **.charset**: `utf-8` +## MultiPartRenderer + +This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing]. + +**.media_type**: `multipart/form-data; boundary=BoUnDaRyStRiNg` + +**.format**: `'.multipart'` + +**.charset**: `utf-8` + --- # Custom renderers @@ -373,6 +383,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [cors]: http://www.w3.org/TR/cors/ [cors-docs]: ../topics/ajax-csrf-cors.md +[testing]: testing.md [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [application/vnd.github+json]: http://developer.github.com/v3/media/ diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 4a5164c9b..7b114983d 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -149,6 +149,33 @@ Default: `None` --- +## Test settings + +*The following settings control the behavior of APIRequestFactory and APIClient* + +#### TEST_REQUEST_DEFAULT_FORMAT + +The default format that should be used when making test requests. + +This should match up with the format of one of the renderer classes in the `TEST_REQUEST_RENDERER_CLASSES` setting. + +Default: `'multipart'` + +#### TEST_REQUEST_RENDERER_CLASSES + +The renderer classes that are supported when building test requests. + +The format of any of these renderer classes may be used when contructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')` + +Default: + + ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer' + ) + +--- + ## Browser overrides *The following settings provide URL or form-based overrides of the default browser behavior.* diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index f3a092a9d..293ee7019 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -10,13 +10,100 @@ REST framework includes a few helper classes that extend Django's existing test # APIRequestFactory -Extends Django's existing `RequestFactory`. +Extends [Django's existing `RequestFactory` class][requestfactory]. -**TODO**: Document making requests. Note difference on form PUT requests. Document configuration. +## Creating test requests + +The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. + +### Using the format arguments + +Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: + + factory = APIRequestFactory() + request = factory.post('/notes/', {'title': 'new idea'}, format='json') + +By default the available formats are `'multipart'` and `'json'`. For compatibility with Django's existing `RequestFactory` the default format is `'multipart'`. + +To support a wider set of request formats, or change the default format, [see the configuration section][configuration]. + +If you need to explictly encode the request body, you can do so by explicitly setting the `content_type` flag. For example: + + request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') + +### PUT and PATCH with form data + +One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`. + +For example, using `APIRequestFactory`, you can make a form PUT request like so: + + factory = APIRequestFactory() + request = factory.put('/notes/547/', {'title': 'remember to email dave'}) + +Using Django's `Factory`, you'd need to explicitly encode the data yourself: + + factory = RequestFactory() + data = {'title': 'remember to email dave'} + content = encode_multipart('BoUnDaRyStRiNg', data) + content_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' + request = factory.put('/notes/547/', content, content_type=content_type) + +## Forcing authentication + +When testing views directly using a request factory, it's often convenient to be able to directly authenticate the request, rather than having to construct the correct authentication credentials. + +To forcibly authenticate a request, use the `force_authenticate()` method. + + factory = APIRequestFactory() + user = User.objects.get(username='olivia') + view = AccountDetail.as_view() + + # Make an authenticated request to the view... + request = factory.get('/accounts/django-superstars/') + force_authenticate(request, user=user) + response = view(request) + +The signature for the method is `force_authenticate(request, user=None, token=None)`. When making the call, either or both of the user and token may be set. + +--- + +**Note**: When using `APIRequestFactory`, the object that is returned is Django's standard `HttpRequest`, and not REST framework's `Request` object, which is only generated once the view is called. + +This means that setting attributes directly on the request object may not always have the effect you expect. For example, setting `.token` directly will have no effect, and setting `.user` directly will only work if session authentication is being used. + + # Request will only authenticate if `SessionAuthentication` is in use. + request = factory.get('/accounts/django-superstars/') + request.user = user + response = view(request) + +--- + +## Forcing CSRF validation + +By default, requests created with `APIRequestFactory` will not have CSRF validation applied when passed to a REST framework view. If you need to explicitly turn CSRF validation on, you can do so by setting the `enforce_csrf_checks` flag when instantiating the factory. + + factory = APIRequestFactory(enforce_csrf_checks=True) + +--- + +**Note**: It's worth noting that Django's standard `RequestFactory` doesn't need to include this option, because when using regular Django the CSRF validation takes place in middleware, which is not run when testing views directly. When using REST framework, CSRF validation takes place inside the view, so the request factory needs to disable view-level CSRF checks. + +--- # APIClient -Extends Django's existing `Client`. +Extends [Django's existing `Client` class][client]. + +## Making requests + +The `APIClient` class supports the same request interface as `APIRequestFactory`. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example: + + client = APIClient() + client.post('/notes/', {'title': 'new idea'}, format='json') + +To support a wider set of request formats, or change the default format, [see the configuration section][configuration]. + +## Authenticating ### .login(**kwargs) @@ -59,17 +146,23 @@ This can be a useful shortcut if you're testing the API but don't want to have t >>> client = APIClient() >>> client.force_authenticate(user=user) -To unauthenticate subsequant requests, call `force_authenticate` setting the user and/or token to `None`. +To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`. >>> client.force_authenticate(user=None) -### Making requests +## CSRF validation -**TODO**: Document requests similarly to `APIRequestFactory` +By default CSRF validation is not applied when using `APIClient`. If you need to explicitly enable CSRF validation, you can do so by setting the `enforce_csrf_checks` flag when instantiating the client. + + client = APIClient(enforce_csrf_checks=True) + +As usual CSRF validation will only apply to any session authenticated views. This means CSRF validation will only occur if the client has been logged in by calling `login()`. + +--- # Testing responses -### Using request.data +## Checking the response data When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. @@ -83,7 +176,7 @@ Instead of inspecting the result of parsing `request.content`: response = self.client.get('/users/4/') self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) -### Rendering responses +## Rendering responses If you're testing views directly using `APIRequestFactory`, the responses that are returned will not yet be rendered, as rendering of template responses is performed by Django's internal request-response cycle. In order to access `response.content`, you'll first need to render the response. @@ -92,6 +185,36 @@ If you're testing views directly using `APIRequestFactory`, the responses that a response = view(request, pk='4') response.render() # Cannot access `response.content` without this. self.assertEqual(response.content, '{"username": "lauren", "id": 4}') - -[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper \ No newline at end of file +--- + +# Configuration + +## Setting the default format + +The default format used to make test requests may be set using the `TEST_REQUEST_DEFAULT_FORMAT` setting key. For example, to always use JSON for test requests by default instead of standard multipart form requests, set the following in your `settings.py` file: + + REST_FRAMEWORK = { + ... + 'TEST_REQUEST_DEFAULT_FORMAT': 'json' + } + +## Setting the available formats + +If you need to test requests using something other than multipart or json requests, you can do so by setting the `TEST_REQUEST_RENDERER_CLASSES` setting. + +For example, to add support for using `format='yaml'` in test requests, you might have something like this in your `settings.py` file. + + REST_FRAMEWORK = { + ... + 'TEST_REQUEST_RENDERER_CLASSES': ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.YAMLRenderer' + ) + } + +[cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper +[client]: https://docs.djangoproject.com/en/dev/topics/testing/overview/#module-django.test.client +[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory +[configuration]: #configuration diff --git a/rest_framework/response.py b/rest_framework/response.py index c4b2aaa66..5877c8a3e 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -50,7 +50,7 @@ class Response(SimpleTemplateResponse): charset = renderer.charset content_type = self.content_type - if content_type is None and charset is not None and ';' not in media_type: + if content_type is None and charset is not None: content_type = "{0}; charset={1}".format(media_type, charset) elif content_type is None: content_type = media_type diff --git a/rest_framework/settings.py b/rest_framework/settings.py index beb511aca..8fd177d58 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -73,6 +73,13 @@ DEFAULTS = { 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, + # Testing + 'TEST_REQUEST_RENDERER_CLASSES': ( + 'rest_framework.renderers.MultiPartRenderer', + 'rest_framework.renderers.JSONRenderer' + ), + 'TEST_REQUEST_DEFAULT_FORMAT': 'multipart', + # Browser enhancements 'FORM_METHOD_OVERRIDE': '_method', 'FORM_CONTENT_OVERRIDE': '_content', @@ -115,6 +122,7 @@ IMPORT_STRINGS = ( 'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_FILTER_BACKENDS', 'FILTER_BACKEND', + 'TEST_REQUEST_RENDERER_CLASSES', 'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_TOKEN', ) diff --git a/rest_framework/test.py b/rest_framework/test.py index 2f658a56c..29d017ee4 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -1,22 +1,31 @@ # -- coding: utf-8 -- -# Note that we use `DjangoRequestFactory` and `DjangoClient` names in order +# Note that we import as `DjangoRequestFactory` and `DjangoClient` in order # to make it harder for the user to import the wrong thing without realizing. from __future__ import unicode_literals from django.conf import settings from django.test.client import Client as DjangoClient from django.test.client import ClientHandler +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.renderers import JSONRenderer, MultiPartRenderer + + +def force_authenticate(request, user=None, token=None): + request._force_auth_user = user + request._force_auth_token = token class APIRequestFactory(DjangoRequestFactory): - renderer_classes = { - 'json': JSONRenderer, - 'multipart': MultiPartRenderer - } - default_format = 'multipart' + renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES + default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT + + def __init__(self, enforce_csrf_checks=False, **defaults): + self.enforce_csrf_checks = enforce_csrf_checks + self.renderer_classes = {} + for cls in self.renderer_classes_list: + self.renderer_classes[cls.format] = cls + super(APIRequestFactory, self).__init__(**defaults) def _encode_data(self, data, format=None, content_type=None): """ @@ -35,18 +44,24 @@ class APIRequestFactory(DjangoRequestFactory): ret = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET) else: - # Use format and render the data into a bytestring 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( + format, + ', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()]) + ) + ) + + # Use format and render the data into a bytestring renderer = self.renderer_classes[format]() ret = renderer.render(data) # Determine the content-type header from the renderer - if ';' in renderer.media_type: - content_type = renderer.media_type - else: - content_type = "{0}; charset={1}".format( - renderer.media_type, renderer.charset - ) + content_type = "{0}; charset={1}".format( + renderer.media_type, renderer.charset + ) # Coerce text to bytes if required. if isinstance(ret, six.text_type): @@ -74,6 +89,11 @@ class APIRequestFactory(DjangoRequestFactory): data, content_type = self._encode_data(data, format, content_type) return self.generic('OPTIONS', path, data, content_type, **extra) + def request(self, **kwargs): + request = super(APIRequestFactory, self).request(**kwargs) + request._dont_enforce_csrf_checks = not self.enforce_csrf_checks + return request + class ForceAuthClientHandler(ClientHandler): """ @@ -82,25 +102,21 @@ class ForceAuthClientHandler(ClientHandler): """ def __init__(self, *args, **kwargs): - self._force_auth_user = None - self._force_auth_token = None + self._force_user = None + self._force_token = None super(ForceAuthClientHandler, self).__init__(*args, **kwargs) def get_response(self, request): # This is the simplest place we can hook into to patch the # request object. - request._force_auth_user = self._force_auth_user - request._force_auth_token = self._force_auth_token + force_authenticate(request, self._force_user, self._force_token) return super(ForceAuthClientHandler, self).get_response(request) class APIClient(APIRequestFactory, DjangoClient): def __init__(self, enforce_csrf_checks=False, **defaults): - # Note that our super call skips Client.__init__ - # since we don't need to instantiate a regular ClientHandler - super(DjangoClient, self).__init__(**defaults) + super(APIClient, self).__init__(**defaults) self.handler = ForceAuthClientHandler(enforce_csrf_checks) - self.exc_info = None self._credentials = {} def credentials(self, **kwargs): @@ -114,10 +130,10 @@ class APIClient(APIRequestFactory, DjangoClient): Forcibly authenticates outgoing requests with the given user and/or token. """ - self.handler._force_auth_user = user - self.handler._force_auth_token = token + self.handler._force_user = user + self.handler._force_token = token - def request(self, **request): + def request(self, **kwargs): # Ensure that any credentials set get added to every request. - request.update(self._credentials) - return super(APIClient, self).request(**request) + kwargs.update(self._credentials) + return super(APIClient, self).request(**kwargs) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 3706f38c2..49d45fc29 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -6,11 +6,11 @@ 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 +from rest_framework.test import APIClient, APIRequestFactory, force_authenticate @api_view(['GET', 'POST']) -def mirror(request): +def view(request): return Response({ 'auth': request.META.get('HTTP_AUTHORIZATION', b''), 'user': request.user.username @@ -18,11 +18,11 @@ def mirror(request): urlpatterns = patterns('', - url(r'^view/$', mirror), + url(r'^view/$', view), ) -class CheckTestClient(TestCase): +class TestAPITestClient(TestCase): urls = 'rest_framework.tests.test_testing' def setUp(self): @@ -66,3 +66,50 @@ class CheckTestClient(TestCase): 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') From 5427d90fa48398684948067530cd8083f785c248 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Jul 2013 17:22:11 +0100 Subject: [PATCH 11/48] Remove console style from code blocks --- docs/api-guide/testing.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 293ee7019..a48aff00e 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -16,7 +16,7 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. -### Using the format arguments +#### Using the format arguments Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: @@ -31,7 +31,7 @@ If you need to explictly encode the request body, you can do so by explicitly se request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') -### PUT and PATCH with form data +#### PUT and PATCH with form data One difference worth noting between Django's `RequestFactory` and REST framework's `APIRequestFactory` is that multipart form data will be encoded for methods other than just `.post()`. @@ -105,50 +105,50 @@ To support a wider set of request formats, or change the default format, [see th ## Authenticating -### .login(**kwargs) +#### .login(**kwargs) The `login` method functions exactly as it does with Django's regular `Client` class. This allows you to authenticate requests against any views which include `SessionAuthentication`. # Make all requests in the context of a logged in session. - >>> client = APIClient() - >>> client.login(username='lauren', password='secret') + client = APIClient() + client.login(username='lauren', password='secret') To logout, call the `logout` method as usual. # Log out - >>> client.logout() + client.logout() The `login` method is appropriate for testing APIs that use session authentication, for example web sites which include AJAX interaction with the API. -### .credentials(**kwargs) +#### .credentials(**kwargs) The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client. # Include an appropriate `Authorization:` header on all requests. - >>> token = Token.objects.get(username='lauren') - >>> client = APIClient() - >>> client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) + token = Token.objects.get(username='lauren') + client = APIClient() + client.credentials(HTTP_AUTHORIZATION='Token ' + token.key) Note that calling `credentials` a second time overwrites any existing credentials. You can unset any existing credentials by calling the method with no arguments. # Stop including any credentials - >>> client.credentials() + client.credentials() The `credentials` method is appropriate for testing APIs that require authentication headers, such as basic authentication, OAuth1a and OAuth2 authentication, and simple token authentication schemes. -### .force_authenticate(user=None, token=None) +#### .force_authenticate(user=None, token=None) Sometimes you may want to bypass authentication, and simple force all requests by the test client to be automatically treated as authenticated. This can be a useful shortcut if you're testing the API but don't want to have to construct valid authentication credentials in order to make test requests. - >>> user = User.objects.get(username='lauren') - >>> client = APIClient() - >>> client.force_authenticate(user=user) + user = User.objects.get(username='lauren') + client = APIClient() + client.force_authenticate(user=user) To unauthenticate subsequent requests, call `force_authenticate` setting the user and/or token to `None`. - >>> client.force_authenticate(user=None) + client.force_authenticate(user=None) ## CSRF validation From 7398464b397d37dbcfda13eb6142039fed3e9a19 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Jul 2013 13:08:43 +0100 Subject: [PATCH 12/48] Tweak docs --- docs/api-guide/testing.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index a48aff00e..aba9283e0 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -16,10 +16,15 @@ Extends [Django's existing `RequestFactory` class][requestfactory]. The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. -#### Using the format arguments + # Using the standard RequestFactory API to create a form POST request + factory = APIRequestFactory() + request = factory.post('/notes/', {'title': 'new idea'}) + +#### Using the `format` argument Methods which create a request body, such as `post`, `put` and `patch`, include a `format` argument, which make it easy to generate requests using a content type other than multipart form data. For example: + # Create a JSON POST request factory = APIRequestFactory() request = factory.post('/notes/', {'title': 'new idea'}, format='json') @@ -27,7 +32,9 @@ By default the available formats are `'multipart'` and `'json'`. For compatibil To support a wider set of request formats, or change the default format, [see the configuration section][configuration]. -If you need to explictly encode the request body, you can do so by explicitly setting the `content_type` flag. For example: +#### Explicitly encoding the request body + +If you need to explictly encode the request body, you can do so by setting the `content_type` flag. For example: request = factory.post('/notes/', json.dumps({'title': 'new idea'}), content_type='application/json') @@ -40,7 +47,7 @@ For example, using `APIRequestFactory`, you can make a form PUT request like so: factory = APIRequestFactory() request = factory.put('/notes/547/', {'title': 'remember to email dave'}) -Using Django's `Factory`, you'd need to explicitly encode the data yourself: +Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself: factory = RequestFactory() data = {'title': 'remember to email dave'} From 7d43f41e4aa50c4258ec1d7b63dd62a01440fa9d Mon Sep 17 00:00:00 2001 From: Andy Freeland Date: Thu, 4 Jul 2013 01:51:24 -0400 Subject: [PATCH 13/48] Remove 'Hold down "Control" ...' message from help_text When getting the help_text from a field where `many=True`, Django appends 'Hold down "Control", or "Command" on a Mac, to select more than one.' to the help_text. This makes some sense in Django's ModelForms, but no sense in the API. --- rest_framework/fields.py | 15 ++++++++++++++- rest_framework/tests/models.py | 2 +- rest_framework/tests/test_serializer.py | 12 ++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 35848b4ce..1a0ad3b94 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -100,6 +100,19 @@ def humanize_strptime(format_string): return format_string +def strip_multiple_choice_msg(help_text): + """ + Remove the 'Hold down "control" ...' message that is enforced in select + multiple fields. + + 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 = unicode(multiple_choice_msg) + + return help_text.replace(multiple_choice_msg, '') + + class Field(object): read_only = True creation_counter = 0 @@ -122,7 +135,7 @@ class Field(object): self.label = smart_text(label) if help_text is not None: - self.help_text = smart_text(help_text) + self.help_text = strip_multiple_choice_msg(smart_text(help_text)) def initialize(self, parent, field_name): """ diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index e2d4eacdc..1598ecd94 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,7 +52,7 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): - rel = models.ManyToManyField(Anchor) + rel = models.ManyToManyField(Anchor, help_text='Some help text.') class ReadOnlyManyToManyModel(RESTFrameworkModel): diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8b87a0847..6c18f15ca 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1376,6 +1376,18 @@ class FieldLabelTest(TestCase): 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.', unicode(rel_field.help_text)) + + class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): From b88bdfb9a5b82fce89a0e8f263e92e0817c9241d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Jul 2013 13:30:56 +0100 Subject: [PATCH 14/48] Add section on dynamically modifying fields. Refs #958 --- docs/api-guide/serializers.md | 43 ++++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index f8761cb2b..dc16ba837 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -423,6 +423,47 @@ You can create customized subclasses of `ModelSerializer` or `HyperlinkedModelSe Doing so should be considered advanced usage, and will only be needed if you have some particular serializer requirements that you often need to repeat. +## Dynamically modifiying fields + +Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer. + +### Example + +For example, if you wanted to be able to set which fields should be used by a serializer at the point of initializing it, you could create a serializer class like so: + + class DynamicFieldsModelSerializer(serializers.ModelSerializer): + """ + A ModelSerializer that takes an additional `fields` argument that + controls which fields should be displayed. + """ + + def __init__(self, *args, **kwargs): + # Don't pass the 'fields' arg up to the superclass + fields = kwargs.pop('fields', None) + + # Instatiate the superclass normally + super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) + + if fields: + # Drop any fields that are not specified in the `fields` argument. + allowed = set(fields) + existing = set(self.fields.keys()) + for field_name in existing - allowed: + self.fields.pop(field_name) + +This would then allow you to do the following: + + >>> class UserSerializer(DynamicFieldsModelSerializer): + >>> class Meta: + >>> model = User + >>> fields = ('id', 'username', 'email') + >>> + >>> print UserSerializer(user) + {'id': 2, 'username': 'jonwatts', 'email': 'jon@example.com'} + >>> + >>> print UserSerializer(user, fields=('id', 'email')) + {'id': 2, 'email': 'jon@example.com'} + ## Customising the default fields The `field_mapping` attribute is a dictionary that maps model classes to serializer classes. Overriding the attribute will let you set a different set of default serializer classes. @@ -457,7 +498,7 @@ Note that the `model_field` argument will be `None` for reverse relationships. Returns the field instance that should be used for non-relational, non-pk fields. -## Example +### Example The following custom model serializer could be used as a base class for model serializers that should always exclude the pk by default. From a890116ab31e57af3bd1382c1f17259fa368f988 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Jul 2013 13:49:28 +0100 Subject: [PATCH 15/48] Minor docs addition --- docs/api-guide/serializers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index dc16ba837..8e9de10e0 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -427,6 +427,8 @@ Doing so should be considered advanced usage, and will only be needed if you hav Once a serializer has been initialized, the dictionary of fields that are set on the serializer may be accessed using the `.fields` attribute. Accessing and modifying this attribute allows you to dynamically modify the serializer. +Modifying the `fields` argument directly allows you to do interesting things such as changing the arguments on serializer fields at runtime, rather than at the point of declaring the serializer. + ### Example For example, if you wanted to be able to set which fields should be used by a serializer at the point of initializing it, you could create a serializer class like so: From 8f79caf9d1bd4a3de8371c61f24dcf513454f06b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Jul 2013 09:07:18 +0100 Subject: [PATCH 16/48] Use 'force_text', not 'unicode', for compat across python version --- rest_framework/fields.py | 6 +++--- rest_framework/tests/test_serializer.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1a0ad3b94..6e5ee470a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -102,13 +102,13 @@ def humanize_strptime(format_string): def strip_multiple_choice_msg(help_text): """ - Remove the 'Hold down "control" ...' message that is enforced in select - multiple fields. + Remove the 'Hold down "control" ...' message that is Django enforces in + select multiple fields on ModelForms. (Required for 1.5 and earlier) 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 = unicode(multiple_choice_msg) + multiple_choice_msg = force_text(multiple_choice_msg) return help_text.replace(multiple_choice_msg, '') diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 6c18f15ca..38acc3540 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1385,7 +1385,7 @@ class ManyFieldHelpTextTest(TestCase): 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.', unicode(rel_field.help_text)) + self.assertEqual('Some help text.', rel_field.help_text) class AttributeMappingOnAutogeneratedFieldsTests(TestCase): From b05eba70bdec553f7d629a5b4c11afb2a9c8e923 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Jul 2013 10:38:21 +0100 Subject: [PATCH 17/48] Drop invalid download_url from pypi setup --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ebaaf9828..adf083cbf 100755 --- a/setup.py +++ b/setup.py @@ -55,7 +55,6 @@ setup( name='djangorestframework', version=version, url='http://django-rest-framework.org', - download_url='http://pypi.python.org/pypi/rest_framework/', license='BSD', description='Web APIs for Django, made easy.', author='Tom Christie', From 997a93ac2e2f1e7ac9d7f9d6a7e477752763c38a Mon Sep 17 00:00:00 2001 From: Sebastien Beal Date: Sun, 7 Jul 2013 19:04:47 +0900 Subject: [PATCH 18/48] Add info about autocomplete widgets for browsable API --- docs/topics/browsable-api.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index 85f1faff2..8150629c3 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -24,8 +24,8 @@ To customize the default style, create a template called `rest_framework/api.htm **templates/rest_framework/api.html** {% extends "rest_framework/base.html" %} - - ... # Override blocks with required customizations + + ... # Override blocks with required customizations ### Overriding the default theme @@ -96,7 +96,7 @@ You can add your site name or branding by including the branding block: {% block branding %}

My Site Name

{% endblock %} - + You can also customize the style by adding the `bootstrap_theme` or `style` block similar to `api.html`. ### Advanced Customization @@ -125,6 +125,28 @@ The context that's available to the template: For more advanced customization, such as not having a Bootstrap basis or tighter integration with the rest of your site, you can simply choose not to have `api.html` extend `base.html`. Then the page content and capabilities are entirely up to you. +#### Autocompletion + +When a `ChoiceField` has too many items, the browsable API rendering can become very slow because of all of the options loading. One solution is to replace the selector by an autocomplete widget for example using [django-autocomplete-light]. In addition to the default steps to set up your Autocomplete class, add the the following to the `api.html` template: + + {% block script %} + {{ block.super }} + {% include 'autocomplete_light/static.html' %} + {% endblock %} + +And add the `autocomplete_light.ChoiceWidget` for your Autocomplete class to the serializer field. + + import autocomplete_light + + class BookSerializer(serializers.ModelSerializer): + author = serializers.ChoiceField( + widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete') + ) + + class Meta: + model = Book + + [cite]: http://en.wikiquote.org/wiki/Alfred_North_Whitehead [drfreverse]: ../api-guide/reverse.md [ffjsonview]: https://addons.mozilla.org/en-US/firefox/addon/jsonview/ @@ -136,4 +158,4 @@ For more advanced customization, such as not having a Bootstrap basis or tighter [bswatch]: http://bootswatch.com/ [bcomponents]: http://twitter.github.com/bootstrap/components.html [bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar - +[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light From db863be10cca5d43e37cace88fd2a500f6ee96f8 Mon Sep 17 00:00:00 2001 From: Gertjan Oude Lohuis Date: Tue, 9 Jul 2013 12:19:13 +0200 Subject: [PATCH 19/48] Add an ModelAdmin for easy management of Tokens --- rest_framework/authtoken/admin.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 rest_framework/authtoken/admin.py diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py new file mode 100644 index 000000000..0d9481604 --- /dev/null +++ b/rest_framework/authtoken/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from .models import Token + + +class TokenAdmin(admin.ModelAdmin): + list_display = ('key', 'user', 'created') + fields = ('user',) + ordering = ('-created',) + + +admin.site.register(Token, TokenAdmin) From 3032a06c9bd8cc98387b14f7feceb1f5d76041fd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Jul 2013 13:12:28 +0100 Subject: [PATCH 20/48] Use absolute import style --- rest_framework/authtoken/admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/admin.py b/rest_framework/authtoken/admin.py index 0d9481604..ec28eb1ca 100644 --- a/rest_framework/authtoken/admin.py +++ b/rest_framework/authtoken/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Token +from rest_framework.authtoken.models import Token class TokenAdmin(admin.ModelAdmin): From f94ce6e7cbb0fe95725d41ece8cfb249248ac6ea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Jul 2013 13:13:56 +0100 Subject: [PATCH 21/48] Added @gertjanol for work on #976 - thank you :) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6fb9134e..42b28d263 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -145,6 +145,7 @@ The following people have helped make REST framework great. * Philip Douglas - [freakydug] * Igor Kalat - [trwired] * Rudolf Olah - [omouse] +* Gertjan Oude Lohuis - [gertjanol] Many thanks to everyone who's contributed to the project. @@ -326,3 +327,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [freakydug]: https://github.com/freakydug [trwired]: https://github.com/trwired [omouse]: https://github.com/omouse +[gertjanol]: https://github.com/gertjanol From 77364d2b84aa20030f579076a4b2a92a2c9d5dd4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Jul 2013 14:00:53 +0100 Subject: [PATCH 22/48] Add screenshot, and tweak docs --- docs/img/autocomplete.png | Bin 0 -> 58140 bytes docs/topics/browsable-api.md | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 docs/img/autocomplete.png diff --git a/docs/img/autocomplete.png b/docs/img/autocomplete.png new file mode 100644 index 0000000000000000000000000000000000000000..29075b257ccfc85c0939fd9ddcc000243c0ab4ba GIT binary patch literal 58140 zcmd3NWmuHm*DoLn5`t3F3Ifv79TFnlAPvIMFq8}(Dj<#Y(A_Z7F@)0H-Ho*55W{fh zdH#>_yze>Jbv~SPox_K@ZtmH8t-bbIYwzER306^(!NnrOLP0^nm3=LxhJu2ghJu0? zg^7lIqgu=WLqWm%VkIf5A}cBRM8yecZek0hU9PJiwodZQ|pQ;6O0JZrg#PzU?%w~+TV=XZ59#^06u-HmJVkF47g zT0+1S2m9g2{mx55`)Ma=;$;?C=%in8P{42slaDPVtZbtGfu86N-I18XU&DHOiQNJN z?tbuP9Rr~}qiO`OMVvuuZbhE-ZJJf0__ao@n7Q&Y6RfPgW;MJ2Q2-_5`=_#6PMIg0 zQXe-0y3rpJe-e0?7TTU7JT&2)R)7g?cg92!2KNx=p^5KExahJm69r2)gnl7PA6UC1 z-buUHxym_%iSfiX9$YWc{|WCg-7T%sj#y%kLVUX!r#hW*3-P+_dWUGE?aO;yz6V3M zl|B9VPp^5jxj)CJq*9T7Je4vVI)0G3L8^4MJg!-v@YeI`<@FSH5qo4#h9 zS`OV+GJ5#z=p;r`g2Y9uhOYv4!hv0@fJLL5?k+xgFpVN)d{_JdgR+;H9}Medb5ug_K&Cql zU%Kfl(;qs-=G^4WHhNDG5$#kciwmjOqEDpZpwfGnZ@qY;xP-0@oXJo!CQwmw`wx?- z$OKl~P+a88rCR4N-9DkC+|w!Kj>2FPC-{y=YaG!(fLe!ww~USU^7$vUBV$ryRJLw( zi7$^z?vaagJin{=?STsRxnI*V3O`Q7dnJcQDR)V}X*v+#_+uEOhkZyeex&e$6!X0U z{@pAIrYPbZJg@{WKBdzKuT10TXdyw-85*DPf-@$DUlkA*`B`V)D8`_W_)s?I)lCZya5$C9SYP*af@*VVXRgf;QWTKZk~l}fD&ZGm=yvL#DWNM7c^ z2-|lQ7w)8hK?#U-LZ;0K#xUjglka-pRZpK?kq5q4?Mi%`!hxeeK~GfqNIk5umD^;N zgXD!|qGLZdaj0fH*;~qT)M+5te6ehWRbP48`yON5h zO^#~n2{wD^j_od1ALJDYkC4cGtRnpUek?^Vu^iV1#RkVlHlb#(M99;QcwGifhKZ*< z&(Cy|o|LlhGd3wSh&RYxC7$ALYjdKL-XR5&=Jo4>1;GTIZJg>A8ag&JDl-Z*oHHLP z5&-FaE9*GxUK`U+(oUWmgFWG@su~7Gd~wnXuNP#R#3>WHddYja*2mW6H}LvkeacMT zjMI$m&(|5p8B^3At4cAFJ?A9kBJ3lIWLi{(m*ixw7q+X;mW&s17o5J-QJj$kXX|Ip z=LzS)bprBMi=)db6m!)#J~Qb~Nb%^u%iXb`z%GJ}uTSWYzpXdAL(N z4l6QJPaho~|1pJcIc}LU=9XJnu$>9c(`z5i(h=(G~vPO0Xw^Y<>Fn2 zP`VMl!MhiDul!yzjv#ItITJZ24mOS+4mFu-*nD_J7+JVIxoP-z=XNJF44Cwl{+wPv zMlgmv?@Qqa#vzx4Z9me^{+=-4q$RJd5-*H$vASw@{rw%U-i& zwq$cV@onVg_u-uvD+ODL>u(PE6%z7;M*6-dq`XivRN75Z67+U*br^IM-GL{B*2_2a zy-e5&#UXJd-q+*aEL!Vzv2&nw%&w=azuDtmE!!V-0lN6EX6}AFw>);+wAur2x_6&c zC&VQT7ccC7sI7#N9a2%pMz$yiE4M&WfK|ZKLsK!z7L69-7Fl1;(dMNjmuKgJ|odWBTL4|3LTbB1*$JNx_K>3q+L&LlV7Sox+jnO`E zUnHGL^xK#R^xh_coaA8jp0M?+b=i168-`uFxeo?CY0Gs^O#DM?`sz$-Y55I#_R6Wt z4bH`N5Vyrf-Noc8zbX+c>QTutSpJZCyLlC0l`Do*`Nzh^HvEK{TYJ6qF@7Re$X!~U zK^>tA`hb_MdA83ztz5MugrmRc6Oab`;D>KEY)o$W=#y3+ze%&D5z^uCn)Py1lwX{)v2)PoKStspPp=3x}&I z=<}{agej&R&-(_ek-Cb-*=}zGOX2)cA)sWueyDr}De=-m;Fn}?E7hn68H7hRe!c z!g_IbIB#0Z1D)a_rTdZ1CVT`13p?HZ9ci6RoZjHc+<=)0CMm-?GoJ@b$4Z#|)~CUq0_$b7DR3(;6FjZ3#> zf*bXvt?8j5b1k25-h~Kjc=fg3aBWZ}xUv#0FG?T*J0pr%Nfvc^-v{OS5sC_shB0c~ zD@|z2Z`0ZNV~0RPT}Yqrkr$gV-}`myQp}I1D8`YerLTd0r-V6y?kM;hsK$DFveFuk zJxb-(;5(njgLCLLD=QD9=J73XzD}*o%zy`HW*~~MMSZCK(zY+*x_esu7=HGIE@q== z9(cf;`IB2q#64f*ABMQDO_3-lsA*OjZ(QCeDhQea?buArfNue8ZgvhxY>R>->?VkO zv;(-9JaMzLwRaYD6QTKggdp&cTR z!cJ!9f@)H){+W*aPlU$O#l=C8ogD-Mv4ObQfKC?d90CFY>@PUkIXPL8BUqi??OjaV zSnZu@|3l;_9Vvjbsgsq1ixtrR$zOC$-U8pbh|tjd#pu`Hf7S_bv--`+-uWM~kOH#* z^$j}*+Y9zzw2@PV|9UH^V&w*~eIsRM2e5ZW@(|_V;O7+nd%}PF>Nm?jrt18j%FDz1 z=gdF8`Ddmu`(G0LA<=)f>+iS7{Sw6zX8*PKqF6T)uPabc#8G6WUTV0Z?k(Kck9~*u zdbETyl|k)RWuu9J=pupX1-@1m;d_&d7-dC}APN8yOv==(=MC2^5 zDnG2&CFig9jer|Z{#zSbjp7z3aGAO9%_~O4lRM0W^FDU*Phn(TcE^cW7lYEWw$2jB+2_exte^T`G zTfoMjow>mu{TD@Xzu}0XUu^$gfH4g1qpPdfN0(o0|6ah8sx_|P3;gS8x(OvSbph`w zJ6JbaLIXDWJbs; zXY?a%&0hXXC?*<*<8@i2gQO~Lt*>i*cA{Ve+S}W|dKEZgX}j3yu5&GIsjbB^O8W;R zap}A3Ua2^yJCmh)Bkw6VZqt1cjBN`QgA0zOskA=C-wFQEB#)kWSzD~SD-Sfhw&a}? zz4}#bwTA?>BIGmGpK4%YdIEurS>JyZVu2jIQHQ`ESmhB2`Ila%KO=XW5|>y}`d=c1 zkUXhkUJS_ox2`GgBXvDuJfiWZ^@wMUm;*D-iUlZ)Io`^ryo9U@2PoGm`Vl;NnfM*P1j=d$NK5BpI%s=m4 zsF0Cq#KYN%#LSGRe{9DzW@4D*(&?ck=NyF^NT^g9cifXuR9E0Id`YZ~WDSQ^=5DM? zn&U}-rVb2l)WYB;|5~LdNRgDmTxzgc#9=hd0y5=xwmvyi2~q+JsHXL@mbLb9F$_kg z+~nhXBLsvRooQ{N_d?5Wlgc$P6MS24)6Hk!%GKmm9Z*~yFFEt2#sJfsv&XXSia3jE zv~(6JC7{Q>pYeWgmVXh^uoX|%%E##+pHaa(>jt{L-`?S!3119_Q?^jMCg;iRxLVQm zH;^mN7%Wx|a|C%KRy~)&1SOnyK*GhMu7^u> zBzM0b?rE$$L97BeT&i-G8F$BA+FOEQyeC;?-2<5mSMaV2X9 zOyjgu>J}+iha(y`_+BR0@#>tZuikhvastOkD?^QZO?IuqIRI^S!J$Lu_XPqjE>nC( z`1I|4Z>PdGf(zv~Ph%Hi=)Ged_7VbOg2GcSq;~=qV!GH6+X<8eX_})=yrvr?l}FTy zl-cW-*E!+kjC@63-fm&;Gi}P)|KU?bYG2LNFE(Nmz13m-mo-rdsT=wNb$QCKXD=i6 ziDjzO2wmRYU_+;#VP@KR9fCuty`oD3-R~!NZqfHEb3R|0?;zmNUIpz6WA}IWSni8a z3L(lx2Q6M{BzKD*v7;M>5O7L7uW=|HtTfre*NNTa^^S=m-eeA3B{LqI@11LMSZUoj zWj@xlCxPUoL$mH_^062VZeHFM*(p`LIXsAo|Cm5fLpAu$+sil#r^`NZ!ejKTEWUJ{ z&k*Lp=St91c`L9nm#$om?NO!t<*@K0r8+Im5FohZbbWpO&;q`+mp<5Vb;&c-oalAl zyBJWJz=!K~6^y@ha(^yQF#%y~lnDm?t^yS$oH`8X4>M^xmnC*pPO8)9c~mp{2@ab?iSnyx;!l$)Q_B z^lb4mbw$HELYM8k&RGfH8txL`Jz#P&o61s%8HU_)ykU=oS?z9>9|V&C;`=&C4lbt? zFPUrDhH<=~3eUqLKWcXHejN>y^NM@eS5JM*b+8zDNpN;!w6(?mKt@F>IWIUe7vm z)s!sxST8rp-GiB%R4!u0twnnX%Qo*5i*5vnvIbx_o4J|5CW1EmA2TVxYvyvE4{Kde z;nW^|WvMM2MflhG&(`f*;*Y*`r+~}^%T=KlG;9}L)40Vj z>3pv*;)`~a+h+LqBraEci(evcV|`F-Bl@`qx8aH ziLj=2ZXDkt=h$wQZ(X|iSbiYEkeJ)l=UcKWkyoxKE;r|$^;l_&d`SVarLf3#Z1a(u z6(x5*fYtK#)L_HF7F!NBPu2DAS7Xvfom#EZGMTjS2G^>T(YiWa0*#J{6nlW=AD5Ru z8IwcT50bRUyp6Zrs>dy=xp%k$i6?IIHmtVG(E0 zF9PQzl3|;JJ_a5zg16g;M}hJdQpMse5~(iHwS*51YfyuV$FV4{9Vm3W?LiQI% z8a27tA)7n&AYERdev))t=bmlCs|BImepR8>Aw@oyHkCXkXQ-a4@sdK)fLIqt0ayW6`j<3dT%5a%(eId^8Gsd6uKL6HGhmO<4 z(f7B4Lf0_Dk+bbcVR-vzi!<^MH|k0_M`Wksldv|t4vSthU4u}qZ1{$V(t$?)_QCd9 zqwsW=c2L%t^}DMd1+0U!mtr5U;I+U{eFR+khZ^)7a%}qf>0h4nt)$)=ifGye%FA1e zkAyHev`Y_uCgQ7g_2$W8X-q4g=`r`t@p@k0uQ?nz}*ez8;J9mj!%_$0yv-x^BYD zV6%>1JymvJw&5Dp0JT{SH32ADW&Jv4ty573;U5LCbo6KippR4Y zDXk{&pJcztqk>Wg1&K7DzhGAP4H?i&YL$=4Q(!H;3@#QdV6GLv%(`;YmU;E(9B~x? zUL-0l_EMMo!$*+bQl#TIT$c2$@WF4*A)gAokf^kmFIVbcG%}LzV(u4_rtJJHtoi?j z@Rk!HI3^}0DJdx~4!{ZS{hZ43hc`V@>AUM9XuwKR=Li!K7Pe2x_&&oKUPy7s_8xDi8dTyeb|{n#A8p`q!%b#3$~@GE@~eOsa7Hi?N@$05WXr z^?ybDZylH;!-$EJqYR!uW&3|In18Bdb2w?1$(Wa9%30A7L*RG3Y|M_<;oUX%gA!sL z7&%;KtPOde5g*qD54!kDDHn6nDV;3-@QQ3zqqycH21+QE30dSn+gxoZeiM<8?q*&|wyD#+ z%eZv3emj2XU7c2NjDiG+L($Q_G#T)uV#idnajSKBif~plrhpCu-(?>pjY&Q;=Xd;8 zAd6N`=?t`;f;%Y~yIeEwvfL2-RFs&-irAV;-i;COU82 z_S0zJ-;7;yjR6}SRm4Q!`RPjq!DwWp13eHoG7~4YTeriUhrJI0s(FtV$~EIrG7SK- zYD%Q&e3s~CcrsX*$^4HG9FpUOUoz%dJ~FxmQ9|uUi>fsh`NGpAy>vc6xL;Su_02nxV@1 z3u<-DLAP`Qr-yp}9x~Jp+vThruqX>L^-W~Uo9T3_xpp1pNecc&If}vfhAn@OyD0w* z=47qb^-OikrU7P#CiXXmT50bZ$tx``ruS*eU^Z82mD7< z-jzLoDXjgsd7M9W;2Ash&Z6&ApX+a1Uyp`qs@!w$)#UHh1f7qBCe6!9K$C<8BNliC zNPzJ$MOAH6YafB+IWh6MIv+{_y~F8+JkU>a;`@XJ>Rp8EvGqJ`Q@gy1H9agXr-j9w z_qUn$*O2vC-jjeAN)f;=Fcc&&I}<%lgsfCoN>Ihi-X& zeY~!@E-^mM)X9m5QG1?o=^pD4l(xAzEYmD7hm(*on*uFf7Ify1aoo&UfwH;VoB8^e zSAUH_XIX-+gT(4xu;~KnJbJPY%Wqv}4rGWAY4JZ$#}&&;8em zHhe~fHnX3`JI`MLwaH~tMgO5JZb!5XH6jWZ&NLwUwz&G%c`O=_O4#k{tvPtsq_^>+ z?^|0Tpid)@tQX9`J6o8J+ma%zT@w%H9HK;QbW58GFqd-7I7P3X+b>jXx9BxrZhWsN z8)G7S|I4aWDv>9m^`x7ImQAw}B9g)p^%57 zY2u{bR~Q!Qi*s5;JJ?07utBfJ_`vyy`AA8 z2c>)Mk8D;XRmDyx7+t(wzXpOD)Pv{CQsJ)S>viDJw5vU%Y=o!QT7&iR@~vy~l{uHY z_4U-QE0xjpg1>w%-{JLUzxHhLw)2=$i9y}+3F~!P68Ssa`{Y7CDkoPJ{RT0Myp8;b zZ;(y+NznX#BQ~!RrZj$^o#9M{6_;D12E=JgqU3et<(XBk3xj~`e%_MMDd})|{cI`$ zbkfJOaA+*`Ew7Y8^yeY)_gs101<&y~M&;Y9fgdN z1|G+$Khzr9{nm17H`9JOyG`8;KaV`XtGYT1O6)T~%xv?7_KfYs$HUtiSQ#my9;9!5 z;)*5gLgfU`+aeZ@rYf8O5;_{)Mz@m+z0YocY{bXb7y!j62pGnoOEVpIU|VPVv~MyYhw!U~$HKi|({CqoajoRYYG{rVuU8&XyjBb-+iTZiq*-iyxw zgt{HKFS2M)@xojRAP)!6bV0kb^N$jxeUDz+_mSi&Q5K3&p7j>%P4Gln+ zGBe=D3l#6!6bjI+S$i0QoSe%xHhY<|4MaqCdvE}cD{TS5QDzK zraHb&?J4@ZA-&BPCsRw5N`3Eu+lNd2#c3?oTL(*Kcg(K(=R+~xuFl6X?Evyq7pbaG zyp(8-e1UwQ<1S8W84$6BgI<^QqhWcAiy&U7rs(6{r0}3vlo5020CY$U!O>Vx*4@+K zqtNeKG`HuvDo9S_`r8dz(E4WuW*MF`Z!C{yC*2Qi=#3EJ{!uM3XWoV2J$`7Ut+=8Y z-y0TNX?a@fi@4tL2S&o*wI?D5AgS}u@kFnV83?Yc?0-u2a5BxGS`zeluGEz&LyLnH|U_q#J>-m2(^TMz-K ze)ajCLnIE!yFvJf%H#F+^h`WK21km?A142VdYzcO&E1kX43= zb5#D*HnN#qyCu5;k%MU0W4-zokA-HLMYoJ~A9M3$p8A=|8R3!A4`i#Dj-JKhPn)A` zh#>}Y#58-~A~?&`Yv}E$$d-9_PjeetvSk7_RP`BlL-?M@80W9KdOOcD3Q2~4!U@E< z@p!)fWyaQgI#vtp3>orSm?;9&nT$d0NscTkoI9#*DwM+oT~$^E>v_&wT1H?DT%#La zX%*E6qs1!v76NQWp~M?AMMKV@w3`n6rj@;!P?E9r6TUdvHu(NI^ss-4s7%p|r9-z2 zCAMqA9JB2_PDZM1M)q7uhcd~HX<%-s21g!MKupSR2!C*aigH{n`030aam{lUI9p@87Q|()Dw< zY1opaX_NcD>hBt)>m1wfzY}rH%wj_D9#mC4;XWDNCP+BO>Z?z3ShbFnP;7~(U&~sx zsB}7M6D9@qDBkne4R`vMJOJ=lVpEa`E{+{2yiBFLS0o)OCsJd&4{GVNJtKF&tcIS{ z8u?a^XA#qtY;Wjo(SV#sm7QKi-D=w=<+uCq8Lk2BVgkdm-l>nFQl*A-7!#?QL_ z0dpsis(!!&-@>!IeJZbT^L*#&QX@FshN1T3gS2u0u(uM%D5HOTC#>$Ug`l--)}0}N zJivlXn?3vu`~;leu#o$#uel1ez@Zok&pP(i#V+no6&q-UOl25e66k10+8Nv|$bMKk zHPU6mmP<`JPVzp!aCavlu<4itbL>8xqhz3SP>c|Zo6u~!D?fSp!D(R2ZfBo+g41qn z(M)U`X6ls1=JGuQ8n?SNddm;j^Y9Vi>tRgZrB6Px5bq{{6%jaAkQyDnw)gTZ_s_i+ z**_St)|b}iTCB=__UqQh|8fU4Il$I+SU1Vd)W3hBn7A)4=E@TS`3^004{E6kKEtjJ zy*J>U+F`+5L#3>=2z@t8UL2QC`=&O%S^puWC%^Azug9C2&z0uQFNb;>As<^RcE27T zVYE=5tnX?MESkj2k74DMhVX^YZUz-roL-e4oQ!|MKEiNLcIy$7hd29nji~3J+v%Nz zUb&##oNZh9T=^V+E_QM%>!!Du@S^_y?1ZeQ4303CfU`rj?SxrANO|XLNslhBizCnt zW+B&+DS~ld-1jYf6Qw=m-Z!Q|xb=$JD9yOCv?24(XHJdOVrr}g5T(A3n#d`Kuqfm80y7O84dBJl@ zwa)!svjjl-+f?5Dq$Fnug#e*xJx_tUzZLgJYK-R`qG_&INR#T)af@wZ1^s~Xefkd@ znv__9y;W`DqWtw|nfp2$3*LCb%PTBOR0vTf>#dp7&RS1?aHUdTP!5S4&~uE$hP8q; zQfz&TW%T?oWR}?byRvgrnYRSC>tqNx*_7Cv;BzX`N6KHZwSOxbS*1=BkI%{)U9*_? zl}lTo=CxgOdxLG}}tr z7+Xg4bLU%tZ%Ke;umq30rVI~>iECWn6H)Z(b%Np#Q3|h zI>GxS4B=X)lh!uvw^iV79qyCj5IN!Kf1WlK(c5c3xI#H7aw7~_f zn{Z_G1xlp~)@sTnyG->tmU-&w_~xfD7n8<~l3@KV#C}mnB{rMg{wknR$wMX@^nfSB z<=#*HQ|Iq@wE(eY#sI32<(HrVQcb>?n_>8eV66{9`*^;=?X?MG;6H88uL(0TK$bT4 zZsO(Az5U!3o~G3P`0>++PV?tIeZp~{l*&pr)xXzeD-8WSdy$GbhNjaMsJGxAGay<; zH++Ul4C=SL-}s=0&yO2fT~6EA=WD))!e#DH$9k-J7EC`gBl)>D@vrvi$WUrylCc*i z-e=Co3R%GgH0K23MXm7;IoW81J49Ey>LPU4>h381=J@_Ej$ym&(Fz-pH-JV1-*3@w z+Q=$=843olNK!m}Z9UyGR%`;Z@|BJ2Bvxwi3v+^HO$@Pjzbfjra?5gog(e22@Pqs~ z--U#wI}`3gxg1`&C1FpWRu$t&_dbvGg9Ehk_VlaBE_WB|>rbYFIMumAbmDc=Dtdlb zA5_}V*ZG=&4=k$d$7FqA&T0wvJ>Me=${xuC+w3zvjLDLt%jj<`c0P2HK`?9`=%+}> zG;Eeo3|yrn8c!KU3<@91e*76OO(*!JKw=yXVg%_g7lvfL73v$;m3){rnh9pLm94F9 z?(bXgbo0AH`+N7GDV*^w6Rom{`d5LVEO7ozaWj3_x*4aHlVUx+meJg;#n(rjbG~BV zQU8fdVr9gA7Gj13qKqzMuk#9~micV}`uQc(POguR>%zXTgCqbu5z}SDUjt6?XNibc zym_#mYY+D>0Z16mNal1b70pRvI?c4ugf4r5?mQ9FaT>XB)6|xkgDvVchJSeQ*Z4Wz zPd(0I?z?#bkjkHUF3%A+SMa>nKt4m0lcQaYH%W*;*L&GUUrB#)73Di+m7gebd^@*S7Sx%O;YGvut$oDY!In8Kj|Tm zWgApNls1vO)j;B3smM-X=_@rkow`N$lvs^q1zZ7Z;rV)H`CHUKiU9d8|6yhw;dkY& zeCg|3pS@`MjRgWxZ^2rpG~+R+dqA9NK=)Qn1DO+Aat0fM^Z1fCcI0Eib`OrNcP36F zIAbM7lURqpj%CYt`L*W|4^ojh;5f>1l9rvYO`QDmuxH9gTUkX94 zly3BVON|TSl^)G%Vsph2vS+lr>`b_BmMCP;KP_Wk>=}%_s`}9!ZQb3|>i_~J+=e8t zx9r9HuI@J)aZ~@}T=q;EaK)*Ljs10M6+|O@3;ufQx=b!6{;VbzZ9uHf*Ax(IDxA?` zPr(;{)U^OwGT2n>HfBI`a!^-OTQ6=DiHDOIG>w%%TAYzRv7-NJNZR7KD_CYV{o_Zh z87OiajYxN)kdb2y@_RH<=$MMLzYg>9^}Tf>lpu!UTMlRVe>1K-859E809+WRmV4_W zd`I`+?pZ$j4>_Vo%eRNj?b>(5aZ|R;egC~3w2SiCCy0gYI|M2TdJ7ocO5P2eH4|g&)ra5(#rU2 z=-)j$Us(yhNP#Xtr+SGCB-3KQ!IIOL{ow~}eSXU&9Y3@%-9A zDGz?-11D&aSGvgHxZ0bHVVI``v{n28Y>{&{-oGZIx$P|a!PB3K!&(Ku;e+@Q^>V^5 zHw}27+OS{DKM-qo!T+h&Oq8K&XuySrx4~CVu=9;K7#1djb5sVps6HRnsZ3LPkPdhcC?O2#pRDBgi7DLX@FC9pJKv{4(iK z;xfJ-&F?O8HD6Q=h&=RGo|dWXo4xv3p`$SL(-8`yUL{+2**E3gXQm`gX3g1oc9`Sq z?EStv>c*yv`(z9+)%mqHziuTxhkX+1d0EoNPVkvWljzRtyt5QQm)ai4F(8sePOG`L z;>Sw^v+Bu#rKzqi#S`1s4bE8YTvirIA-T`vg1zl-xx+N~|1r5ID&|P7aK?PjtUxbu zGyo75*le)bgNCrz&Pymq-M@O?L;mS73hcNY<7l@`#PZB-jL>U8=2)O(qrjSOoGj#+7030N#$ommK$%{@As!SGLKd(2}8c(1iJ z(_r%nqE+X)bA5hPBjtzP*3$L@J)79g5M)<%4`?mWqpo``EVeeB6;~WDwJ7B6 zG8{GI33L!<;BhxSZMVN#HV2k5WqxfuXQ0p3=gBM58=c(XiZ^=n4WYPE|2CO5GC8cr zW=abEV)wS1b2-?U4uFW~&&;)5$ya98N3Ny`z^BJck&u!J?Su6PLD=O7qYA?U71v;g zNz|o*&f3a*!Eo1vd5m{8nJvT{xFa68s1B_Odoe<~ z#{Gu;?SUN9BVBlSyMbrIS<2*yM*!#)p9d93#c~EEgPlQo_I<(^xU5 z*o4MX?I7a#CaBjn){%)T!twINNiNGPmD{8)ax!r(PpCoeel$)n7?j{3JS^Me^#Ltz zOu}q4WLTT8Sxiv;AUW(f5Ugr?uL&-GxY6Z`%UnMr*I7uA9_5(2pI7su0*EKC!bnzg z$kzmY-Dq2KNC}VZe0%*ZR^~wN>W9Qdk>5}QAOwJTfoAXgvr`Q@v7GGeEqrBqu@K;C+3%t6PM0u{p z1YvYNBMi(CPpv6W5nfSS&ha96JevJsBadal6FvM<=jMTgoeM`-Kts}r`CTTW3yg~% z465gz5>=-s*DN;5Q<-|o<~TN9$M*&v*zT&k)a;kwnA*+leJ0s($4y#=t!kG`9*S!Lhs@;+mtQu3U{d#9>nl zv43=IIag>gW|6YUr~c_v&T0M5+t+JD`yCv~yL}}vmU_np!PEqN8H}(nOLZA!{ff@K zGi>RK_Z?KD)zR^|b0f};S48ubC9D^7A4bkQ$NRA|cB9COuD!rJxJ^=*Nj2Se#q*ZH zmoh5_$8CM0&9t?xnNZpL@!__67qekdtIRq7vwd%?{X_;e(V7Au^*C(0xaTn%Jfreg(jn730qv8^0iId4LmO_~d$UJNXj{b_~}oS~LykalyuNxV9z+`#xMxK+WH1f1$O(xZV&% z&nj3gQ~`2mcdt})|5o^dy-)A*yFh*R#J2X{(&+w0W15|p?qyKPK-eb{s;49t@m`JX zXm(J>IWIQ@sl(;v<{e(A)dr4AK6@WNmAviK)6uudrJ7gI^YJ0TN#ZiW#v7}fo#8Zu z-B~8IF-V5`vdG{L9aw8>wI>*HyvP&}NZCh=v%$qc2h|y(d;WI(bM}mK;g@x%lmdUp z!npG3X|b4ukD{mr>cI+m6z}>52M7E6_k><$Apm5SG~D_rUO1fLF?*B1+A@-hD~} zy;ZDTyq>aMc4M)oOEuGKNK29zur6s}A?LLn-aTX6*(|ZwE!FZ?dQusrnXlCI4{v0_ zDS^$P;ramDH||kZ%}hWNTr4FXepZxYW|N8Zy8ULJmy95}UcJ#HHDWzA&bhaCFnKxl z$OO(AT)E|6r4v6t5v(sy#;9Zl9?q4p1-rCIj4=EP_K|H5lF0B*Ft%_l56xTxnoRcj zCoDnYG5=^}n}b}XOYD3FTbw*<$Ct7_p2oPXJ?RifbB1${z*@fh>WW< zbF%jRU2rvm7UgFo_QX#-06C4RU1qcQ(TilJ;%B|TEl50r7YkX@?^K2Od-EbYWxuLs z%Ko7SB+9c8Gu;?w1xyZR1|m8n^I3%k*6^tQ{sR_2Tk3DC1Q zM?&m^c*v+s&kPF=_n@&ZpwnfcKeUWlQ z$dn9aPn9_&jdErWi3$hq)}Y)@=+J4fkJV1>@^xr}gybzB{7S8$qFYBHHxidDY<4~t zYiM>`w?1@k;iR=r#3smv>WAU2)dil_=gY~=YOequ^iNxrl-aGU+~`}+EnG~ha4|d zvTZ2?!wsBhBoo7A$L@hhD`jlKVw^9&rxLuPLtLBhGCaI9*C&;Fhhbl_KbbU!F+qvS zyyv=eH8uXS>c7Cb$R*$AsbXsC3J$# z+L@Izv2D_GEO<#_j7EbgBnc^C*RrYry`tqmuA3F4KT!C5+Ry$F=R_XYc~CefMtS_B zn>~=7o8|Gs!01T|GlpmNeI4JtwFXwzbZ`8N?f0r>Qk7Q+X8lM^27EuVzxvtn;&%3j!2U(%P#)5lsj^_qA5*tB zfUgAKN*le|`#tQ_!X8hszPtGj-W1aPUiJ(amNuKDkemR;$*$Ft{vOSH51!b3mGPuO3vUcBor2>#%$PN3E6OrX^+^WZp6XwhiT z3U)EM9)?;kHVF24wI^!t_Em=J!QqIL!u6N?hY=Aq^XoZnQ4`Pvyn_6P+R6$fkh3L9 zz9DFKP|9$O4zJjp!6)`m)-mWZrHWbQRLjFE1TL5tcrCIpUw6T~Md#3?tW)(Z0BcCO zTlu#bT_nbngGXI;n=q9!?eTieK_gOgyNFwp0r|k8RtAU)E&o$tZ;TD|e#M1a6Q%X( zva$n6CnjlFVX|uT=5uy;J;(!A7EjRSIS~zq-lsms;N$L``)xsiL3m`m@cM6u_shqk zTC|*Z+8J1+CtwfKW+Yj3iJ1u)_L3Oe8x0Ze32j8IV`>3sJY3ozn;SSdeC1y&*dF+N z;MF@}6yjrT6t`adl_A6AuR;b2l6a6C7q$#L+1hJ%J8H8qLSAN9uL9uEms)c%8gsF= zTIr@WfOYFV1`vep<;@WICf{xjUss#ZW7@A8jkIsZ{xZhY|EY+e%idyBadR6Nwx~{n zTRBeCIO~_v$TL^^w41!6HUF+39+9=krGzK+-*RF;uckrVsT|ZD+y*Wxs;yKg6KBA~}z22dJ8d;O-!Ry_* z?d?RyO2g<~*6?QJ!r9ZYe^V)OkoQ`eJY&rLzN5?6?oYh$Vq*lf-q%0RCn;MXSOkR+x#k9XoB)>66ZSTWzmEcVQ-;J0T$meU6 zZ!NQc94q&bxt_u2n@4l{&j5@56@-Rrq!fQ(W^E+a@uU>Mu&E6@V49|N7efLa#{V6l zQ@HeRpi{rkl7eq|T#7TT^*;eQ)Uo-ROwj}$cWW@HJ*Ate`Ix$$u8DaRIRM1nv{ISo zdV{VW9#;45h>r!_tlMHgN|WmsIQ$Wlj#HtA*M|*-i2jXS=!WFRsRTT94N)&Y$0YZd z^{Up|z7ul$Vcg<#)2p{Wa6%g1TO@P2$A8HLV-?~XicfFl^+c4nisAX*s6o5NqDRFF zOXf6YwR9tRN$i(OeOgS~?ll=6X4wz!{C!3xxrrzKgjk&X=LGC^C3&e@4)>p&zxXD0v7Md@n0$9zui>>`J*Oe2ag)dAHV@g z!W)SzCM*;ce?tc3lQ>bi;&B;qw}DqLjZA3al$iqO918Uh+5mC%g^Zg9L!1Ymh zP7AU`m|!J)o;skEnfHVE57CPKMl4+%9m|_pHn;A9xA!Fo3;4uR^YQ(UL8`?kVqQO%W9;Txdd8oq%NjXVfsIi9 zNOoA~`r%ZJj*S5<8QRJ!(D)MsK2$o745>kU#8VLm~yW&C8FJK;=|H-!rzSXn_&#E2Z6^2e@T{ zVz64@{fFU@a2$Ba;rq)0KrgVyr(*w>c);jJKHrdF$%wZ)S-CTMi&|**qzO!m#0-s#S2quLYZN%s!pJzS zV{NW8ZiPWIyJb(Ci+vSg zrr!}ea!uG$pNdSgqIzubyex*{QH)v{+2M-~gQ9$)^rYyegfPY3`0JMD9C^g`EWFZj ztrN89o)6FOotV%>kkjx(Ln#{GS!riX-|#5?ZJr%0_YAAGic`5(Mf zS9EQ~=y*Tx$5IP;PWS%Md$o9W7545E9BQH4x8v`Pg2Iycr}=nHa)tR!q`lUk7}c-c zbfmdP0u=`z4~y6~xRu@>)VVKGij<{IbKHbY!)-y;|2uB^1h?V?#IrI0ixjP_ zk-#OET4H`rYzJ`w%zVURa$J2f+C8aPJCjqUoxY0< zVk}jP&*69Ax`gu|U$aHM=lkQB)F?;6<}7}kxLT~5p`-3@BUkSpp#$}P7ZW8jrzDar zx^^lmFh~43R#dymaPSXI(NQy=Kb0}uK)ICW(G`@Bb`H~ww;^6An^w3X66yPSH2&4? zQ1sGI=kG$8^`kF=Zw9Hp6}Q9{)6ll%@AY%`&FKuzQQEwg3o#O`kI|2+zb<}r#<40_ z%*(D*!#W#9kF|KLW(NU0QZ62g^ccHs18rg8T!Ns16XOqy{!~~A@(2bH(R$+&Mb&a0 zeF49>nfV{wy=7QbZQJ&5VbF+xw19%Ngmei4k|Q9^fJnD=gQ$ckAkvK@-7$1Y!w|v% zLk`_B)X?!?c;7mBJ=eQ!@3#HlPtP|#EY@1H;yixGzVGL`R^}dujjqNLIcz0Xgi#So zi0_DqoZ5%rCJ1i&G_LsB1ciYZ;KJNyy_w^oHSg%Uvq1_EUFrzgS4Ph(`>yL)#yzN~ zCMK%xdCh@I4U3?Hj&ExGH^BqO19X)5jo5>mrn3o)%wO*YiY517X%;!2Oi%K1+v$~- z153pacC?Cg2nnF{r|c$*R63M#e^+hR5V40w^k0aGhpJue7rGN%_n+vKApR7^i2L;r zr-*AZ9FMhs73MnIY*=@*N2U49+fA$+tlSw<%j10fCcle1FbbC&0R_3*Xy=8_Himpl zIL6Gx+GfYH?*X1z{()7RFZzZ-$kY;z+-tEpYh6tAs;v_1i0>HgGnP7P6f~?WbpyJ=-SgjblBzmFYDY(U&nJ% z=}s4(c6_nq+vn=TPj^X!sjHCP6n8~*Kp*t!F_~kW#4U!=a{(5@*}Z5x|~t+Q~VG({dXmP|2+1E-Am05H<>?1 zB0yT1z&(_0eo9+~|BQ>d>li4kNa~FPps;!!xFu(gtZ&~Eq`iJUi+2L~9}`KOEj|9C ziKG$Ck|5OiWj?4`Ft^x#Ft`kJk-4e6@k0lTPIp8(ULDblF1ee_xRhK$)M4CItL5gi zy)=4*jwr5$<_FB}r)}(W_3)3>$c*rD{zo2N!Gw{VM&yP;UW!`#d4J-w%OmaQxu5J#GqMW6R!a1&2ycV^iPaD9jCs&w&n=SRXLPVI6m` z$HA61dlixn?=zW4Aj}k8o_$Ba99+^{C9e^&=AMV z(%14-gx}J@>%B&XWHmZbCpKC6W*Y_G#V;_q%*#N)U+(^W1fvnLpF?t$Ma2nSVn@J< zSb!^Z?}^OJU#<{x6P&O3+xh|dqOBiyX3Z_0-0p?Y)~1GhY#Y2?;T_bV5F`%TY_P+~$DHJThAk_&3e= zSIu~t@7&zl$RykyOz%8eoXH3v8D-*U$O#Ccm}PdZt{Z}_Z|1g>UbWQz_WdQgBN0nZ z26E3B!Y(EYSBiOB&kE)rG@49>sb>(xf12|6r_d1K=BhrySO)7HA) zUk4*L+RC+&`9c%(05D-AEE`FyZnt$3=@-9h^*`Lj2J9ZXSG!zZ+QAP=s#EDLU)$|1 z-|jz)ANC$8#rf*!JNKoUHUSHJsrReF-Z~gXnW(wZ-q-vwZd86)^R~NlgwJ60;^HFv zmpLOx#MY!R_>8fMVXG}tR5Bme`3|EB|MQ6|8*8bp?x5^M^N9Vv0AQhoO7H5EczhG1 zelx*}1;F`X)WmGID{T*l{gv1wc=^Pb;5P11u2^weqWP$BYtDFWrA%9jReGXAlC9f> zJPqxazJ)9cvLMkil2!+}`(n=eii55=1?3~oZwi!iFS;Y5Nx8gMMqlXislUv2_V?0| z;CSfpvhiqsd{m4-RrgKV_GYbMt~93M^NA*;&hbHLFiiI*66La=(=4-fyDGXVoDHRY zi^E)RdgF7XENfBA-YLVWz;{#YV6?1r?>W`X!?2PPCrXgNJPx><^K zy!maMb7&YDy_RMb?Vij7Zq9&oAhYL4wc&87sYJ1~)cZl<{_Laj=<@F~n}RJuWA+__ zjFL&(_t?v@R`X}wTr`}aS&FW+Hu{ltPYzauBFaiflNM^@L@JLyMF@s{`GUJTUMSN& zbPGIMSHrS^M{WLCh>89*fQBi@_1Rm-*De`$p5WfxpZ9fpuTfl}ne!!9-n_fnHk#ow zB;N{IMQz2lTRC-M3b^)+$>4pYp>d<8KC6)S?@rjSpK@$0V*=g22r_Xf=3Sd5ClB2o z9+BK*i9utjCaRE!>pm`(dEV}-ZYaD`T{RQozJ}$YCRL5d=UPqzzMdu13F!=YxLcc> zOSicjkj+LmI9mhDJK4B4-dc46CF$PUo{MbAsb1+?bGVr40NWiMpC#3eTnLrZg|c1F zmdp`1VB!N7ZJ@fM%6w1M&W!{YUE3*8%_J^o zaX65Yv37MjL@g&RHpU$q$(kVO?4-nN;s`T`bTl4uyOu#RW4h8*e=`h_+IyXwQ1j`| zTF&P;RxBn}dKo{-N3Hws#Rx>Iu%B68$?% zS1-JRH~Z~?j$P~Ya`F-S|8L%#ya^ye@T0(;%joI~=pJ+WVQj(7r;AyKvMK3x0PB#6zz7zm`brVR1 zid=ZO--xlEPnw;hzEt^prs1!n1q1A|a5Lo@^@TelmK+I;F9Tb6m&X6XI%xpZfTcFS zp@mrxsZFtY{*vX4+bAW;|47P8?J7 zi(QJJ2fmuuulyOTJs%07I^6 zO?d9_oLm+0UOE|K*TZvAJ@OFImu{*T3o3HF%j493%gCD5Y-EznU^jquTl)I-nlKms zIcUuG^p^-tL09voS^dZ+8x}-nULlL_`p){)HgEhx2gSV365iV!$QE4uZz6RXw4 z$`cq3UdZvQeeuS(z2hw4uK%lyixW``(6J{e=Ry9}@X!ut;Zm2^(I2Y*zCaEV@tRcr zx=nf@`Ji^FvI}ZYNSym=q{OY>AL-}3W+ez^uz#S4yAyhAo}GMd&MVBV=U;h74&qVr ze8vSm6L~V@N_X%G$D!)zwci>*8IJo*u`YIj{BUY&(}U;+AT(%gVMJu~ykiSv8N)Sk z%3wk7lh(${XC=ZLM;$8@v-}nDTl;zZu%3YNRvf(jwyRUZTES-2JX7Pny@+K@dB|#Y zlx4BWqNvF7o_XBYltBs&!TR0sG4+pHJ5v~&bEh>Yf*p>Dp24m$+8EXf9Jm&l1PVIM z5c?bH!E2k{wNs*rC&|T8+A=5}sGfowJ&jbr8=9*(L?&kj(?Mm#bZM3INlY&u+n`4p z^K@X8Sfgb%AFXAzXfIJ*%iJO7$cS*o=Y?25m#*MBEU<@VeU8A87! z(;$B9%9QL#XqjJ=Jd!qQ3e3?Fe~NX+dz5EA^|5r1bf80d9;FV+2slEy!jX{G$HO_g z(I%Xjq`tVfmT1Cu-HghC1hFD;-WD?N-U_xunqo$tnJ^%TlPpz4f@b(Bl){UUFVGFSDn0QBnb^6RDqW zRy3x$T$p%e4gpRzm&)tLibrwU7EgrT_CH+_F5iG>vPs!&S=^^%x)jB(6mN^WYCAJ8 zm?CfO@|rrMH?=3yc%!LdI5CbdH@AX+{D970QGC#YQjmI2Aj%N3=9+G|xLR9=TrS<+ zb9kronX$}Fj)Ft-7}$IG}BVQLwm^Gcxv{Ig?xv!^D5MM61^4jE+ux zg=%@gQ%7E>Z*!m0fNGt8E+L#;G^2i4qHeV=f1c#^iHR7pcZ-KL1QoNRqSMzaq2!I{ zTJytmhK2CW0dLKY+rkl1krjK-uqzA*7VnMk^p2b4jKze9JG?{&D=ZjFVx;t~JTAU4 z=#m3YtNBcZpkdM+h9wox4@Iahdw$HAF+3+ZY_Bt6r=Yr2;h!mz4^wD!<*A*Y6k_Bl z;^=8MEPtpjb#&8JcyzyH3r{nSl)zo++hW+JAvnlV&8*aQHWx3kD%hN{V>m^6$0vey zhOVk)vm#w$k)4vEWtHM(Me)Z6KArdSua1>Yo&s8)WRjJNa!q7YLW79bW;ZPC#*Ksh zeTQ3QQ+v}zW!KwN(=S~2o3pn}J0sl%FTE9Wyww&GKPFGF5?pDNJqi*%xkE;#<*4u( zUeVsb8da&8?^^Z5v2@Aia}IZU$BX{1+Y_rEW@HW@`z(ixQUTrOPfTQydhf7WQbHW^ zUvQYzxT0eLBo<_BWbq5GIYx$+9zD3 z2;NPZ7WI~{pgARQf|pbU=X&G7m5Prb%|4|t4w*s6Lp#?%J}R4MEb1*^$Qo_`Tv8vc zhXABt-59@IWr>Cqn}3It$ZUqe71Ut{U5u}C_~}|1(${JE>)FbXO_6L81AFQwROE1z zIlrvB6XWwmIHUnqG`z#-_a-l*s(f6S{nKc%)3b%kNK_KB=gYN%>#?^5qf2bZr=XF= zvMr;s*jMCOe%Og$kEE9W7%-gr%Dpl;$@GQYCG)s1XvVsHO%`AhW*NVk zU53xbY9q(v{-6ob$?Yga$jKH;OdUeFK#tHq=NF~?ZmK2z&i5li+H`XFYF5)KiX`shNr|P&nXqEE4cc$ ztFa~s$qOT<06B-1@C}evy^Ak$w&2oO@*~zs-(vR1RXujE>Xa{8_AmEiDH&K|SY1!z zXZ2G(f*qfFhn>xh&lC4`3uD&-35*uOq;lxfkj8_V@nuce+ zJI@ry8IiGd5J}0I!zVJ6OI6link7DG{-K^nZQc_ZQ9=HndbSvx^ zcx(_lp=z_n6ie!7hwR4wG}&C+AFC+p!~-@aV5~?k<6Zn7?>jcG#RX+|Q58k`X#|97 zvy^A3Y?JqQ6T_`*zm(__9}b_sh-oRRJ1FS=0zZt6DFUZy*68tDGn%BtrhVtc^WM&q zKTD0(HJ0J2W}lE^2@);cCaNeta?$-En(rwSAU1qmj=xA6F5i*qnI!VyY5n?4q%lR( z?&|XjoA#>d8>tC)RW7EfEaoFoG7lg>zU0K}&EZ3RA{$w6fN^V%FSUjE96x$t&z13K z(Vy2ZfG&R4`6W*)K{SN_)c-L%sR&k5HLZ>Q_jF|P*r$rjBc9F=6r%$JCj)n(cVbqH z?0BIRaoSqD%Rd{g*hbmt@#96ZKJ`StB*kO{LLwo*Ln79Z+=FoHI?elgbt9Ep)_PB^ z!jGu4mRN5EM#hA?>G;|0t!#Igk^NM^o@)0E2_%F&In5e2zD`ZO^TYAB-f$OR<$ky# z9$|GJLMisGt&wW8IHa=7v_M$wH-K<)VrynEQ}aHy0fZo`$R?c8y*LE;{@jFPu_-xA z`-SPQukW7^@nl_E<58VD1pjxA&xe%2aYLZ@t$1i7bfn<np6$boDp@2_z z`!iJ(F|e0cv>z&Te9gdPzgZ#6B?9Lxw0RB}%R8k#Tc`K!gO;7ggbDHHL9L{qyN0(o zU%d!P$GuCcKP-i}BOEr1C^6H0YJxL5?aY?A)&4;@#J}gj(8OA5npu~Yo}*b6CL5r1 zv<2;~01`Q8@BI~cKk~ezLzhildZ34Xc~vXB%Km?*fDyZfh;gA4RO%3Ex8;AD^VV4V@9+}`~tlAx&3H=cSTo*)I~0W)Bk~Kb}fpF zKvLLJP`6AY;hahh6l^KvXWH;^iBt6_;6w3FqPpmkGX6SV@bA&_iEn+y;#&)5R zKP$K0N;l2ssJ{rfw7}e^(g?Tq15dOoDSHHKdAMkIUEZ^3X3N*Mf2gg_RT+|IM!33d zP2QuXPGWmPcdVD#!a$I||;K*UkIjGh8^>37rry>Be@+YH*mN%s&LxLW(AEVl_eSm6Zo0Wg z78Mn}efzdDQ(7JMbID;jYJa%gXe5!)=ugr&*&fqjEgoh(cylcNjU$^0*qUFq**88& z+c~{Ui(fyr&}v(NKd;7YD7(%pt5kb9ucns4F%m(Q^^5QSTKhuG(jLNhseWej4!fn)4?;h*kJ zVqf|0)Lj70A-E`|QRuws@BYCXJ%^4D-+S|h>*nVX{zkUqzgCKS{ra-9;Ejowk?6Jr zP};ihiJ328S9Ox18*hBE`X3n2)+jvQX;S^sbAYqsdv_@_foql1;;)6~Zjjvj|9i3y zj((CwQVjTyk~oMfMHb;bDmI#{>XI!VqkI>V#X!7r32WlH6{`+k+gsTSqz!CFS=aiR zQbZ@oT_%FA7s0^UJV{;5IT^*=y%Td^k+Ad-3r|ToiLu>d)_2Nifp9SgxU4HVg00oL z80K9VN)p)lYb-@C65F0Fu9-xaaD!}kwYiXB?sWwS)Oo>~4BOAsB#UL-ChMLw(&o`B zYkBkfhz4id`>%Gyk&P4Z(W)w_y<}9x3M-y%+;&}Nb00d}oNR#kdBm~PQ(ujjIT^V^ zPxU$*G!tytbX_t(K(<>Xh5!z0wtfg(Upp|LzW=^&v(+g~tcsunOQ6R4G*hMLnnT@_ zDsr8{2_)0B`n=kJs=2i8E)&gQx~0he#29+;n`*Pg8jG_rBD4&E{a zae0+Ht)}!M6}_3ATeH88z&??D#HA0$b5shhFDJF=R_};=nKt`Df-pEBXHG@DkvNf; z`C#JcXhMcU^f?bEW`cW3`6+R{Wd$3f;>uCgfPY>V*TMSnkHzS@bgZCuLh$uZqsBZ@ z>l!6F$=|c48-8$1VTF(ur;J5V@qU7NXzsbWlw;O7VPks=tW))>D)3p5`+Wv0p1Pt` z-a+;`MsX9%u9@kddVvkyjnP@`v5Uvqg+>pbNdR+(eyyc@6Yw}HsFvQmhjhd$(q@G@aj!GoRR zVDaeOo;u~M6|HRy}`fcxRG-CD- z4i5JB>r^=@h&b(^>`mxZIxIE#V#UP7lyQBG?cg$S<36psb2mtm13a&G=MN7-dqT`ltCwWQ7yNa_t=OjP9)n((4uMw|#S7&oDrI$k{y=aBrdfEUBlp)knEz?+iDziaqq_Yv14Eq9%t-ZHG*=oTVK49jgVj zB^@ogp9!r;EF>lt7zR1_tQ&Q-ZoW^jRgt<;#T?~ZZZa8t&?vb%cla1Y#-)=TMLp|o zTi#qzOlu^t=l@_utdl}%ceu2sgMf9dW5$XrstBZdbEFFRp~YxDsxnq@oC$vjmF(2C=bZLtsD(kIp-&y zj&i`fnFl>J$WWjA8VAcK+Qv%tX=KUw4p~6Q5=Z^X%IOr&t~)s@6Ax1U`WdiWz|dC7 zYS7?>;kLK+Jm2-cA;q&g(P@$sy|ej_xB0=Ww`qGF9t_B(88&#hCGU?dD@2dT$*xNF zuL%3{kqbZIm}E+$bMj5~nax z^4QRwZTYm7O z#tNyh%-7()&r$YGQ^FM!O!>E!f;%6th_>;l1^C6i(#kzlL0sGRo-PY@Uhu{C=18x1 z$lG|?&_=LL9656KO(?rttvN2R*t_Qxuc&x0yLH5Orr~z*`|3OS^r=Br=ymW``aqX&0TRK$6}`@I4=QKJ2~mx0yhp7!!LL9SUa za-O8NSibvqPwP2{ClN^x<`d<@@l3qZvG3a~KBn5Dj~>y@H1mvaIW?DIqw#j1&R7c? zxFzS*^X1yxc=A+)1pKsMp7K4D z&Ee5VoJB^_Hip^l?5iy;si*X>&2w)rit>`DOA2L#^Py#Rf%G>YJ$&RB;`xi$bFPdC zew@uF$R3cWWmKYS4WSPC^r_ljjx#Po&~58zdKRQPh?z^>UFp8!{@7+%&9+~%Z6g;f z9cF9Mh0*AK?O6S*YdFZ+RVE#Nc4=?rZ#3+HZ&wTfY#1L*_D4(ciNq+kfEVGZQ$DR1 zxKYGbX6IV=v)OpIG!S<9T`|tD%eGEENe|-MK0ljz;ij}*@fggbL*m*?Oyb@uKQUkZ zBqEG_{<;CJN|e02AIH~@h5o){Lh=lbMpLuaI%yZbMstMGsCxw zQQvBrI1@V`N50U453|8zp`WB?oZ+ZJEhhasRayK{Y2o^03y~d<(WS0RuT1XN= z$&2l^beAa?3AVeBTl3e}#Ei!2odm9e)6icoNe(+_%Dy0j5uR&I%i$e(@L*5w!|^4X zxpkMv{i;@ug^Mqz_n(u@-TV|+IPZ~~@i^`a1FixeZBZoX7LK+HB4^eciS%Z$Jr)~& z_|Rd!clwar;jBFCea7Q|W4YIZtVW%VGPbh0a+63kYUQKMM!mDf`TN?0b(Pik9F0qe zy*pN|2JvD?;wx$nOqrEQQssy8KXU4~E9)f9G4bVUwxOWY=w-u`6M(01#P#akS{#gZ zduG7`4yW00U|KX8EDZ-ns~(kvc`IP(05!R$dBjsgS;=8#z7iT%?6jW9TtX3rG-u+0z1qmFEwjf!C65w`JgHMLv`+e7sHGo$gW^62H+hkuz4gw5If> z8NjpwhfP09E4Ggprt<6(XUvFA`V(d3=H_N*RyeI`85zy(O?Z0)OEqDUuo)T~Bf444 znR-4g_3oc3zqE8yl_-b$#`y*utOh#iRy@>@KQ;L3RAd((eVC`LHX1M-x3VHSzMX@= zna1qMx%$)km2>T|ashm6TY|lP3Scv8ltYUHjkwXZl@wo@QqI$0LSxp+1^;=AkCnp* z8rS>is{sOIojeDy0|w+Hh%wFJM{`fj|H)ih)IO2u_nY;mgVpiEqX*Xn>guOE8zuwG zsDgrJ$cV~azUAwD_D8;t?Fa}1zTnW+u9y_H+x|F|Pg6U6OT5!OyX=+UwcTDep;7>NL;TBfe>w=-pXf9)8V#~Z zI{eDUOsGgoxtfpWGL<&s2(|)kRnjYf43D3rq_s!Y9v`+>v|`DdZ10}9H)RWZ@4niu zdaKnl*qMHoZ!Cpir9zU|v8QhUUkNHtdU|KTuSNQ>)Qd*Jq#a$J#HxUy4#F(@t}H=p zkF8;RnK&NQ)@dbknVJ>%XgWR9UOI5-=DV^o@8w~cOnWNm1XZ2h!ItgGjJ>+2E1%H| zmuX3qy~QAi0dp!d0fA)7!4hx`m}hl#Yn&(4Z0u$wCzu?Ei_)eraqQ{5+RpDU=4}V3 z*TSzCkraXD_ zWV}Bm+ilT70A+D#ImTHtr?mJdwau4%DG6dflIxu3tBv5Jjj{q;+xrTV9TU@gHy#XG zyv(odYQFhr>w7=xuLyk}rvG&9uiAfYMCDC?o-m4({9>8MHzfp~avuHrf3KeZGZA2d zwalI&^gsgqB^C#if5d1hEjlYQ#d0g|nFmZk5+ozB7&vdc z9g)(rcEMIy9ThO=Yd*HrWI6#Ni{iT<1xR_-Fk-DCJgnG3UNa|)bk@#c)jK=9cXtsn zwl-F!JLu^Ki<7`rq4anct;>wD<2l}R)f#0s7W!!@d67t|eVBo3+O2XZ=;cU_@L7?Q z>e9XexlHoyK`%Q~Ub_{xp@i)k>(hgDMPb69HIBBX=*8pTytxON38}X|rwoY4Us_V$ z@-!PUr91`o@;3?}Tdu+Q{5ue1m93_)x1^sAZZUq<$LfiTI7XhFAnylLKP1QS?H1P3 z@lIc-Rv*9@Un&9wC9Ew#P$J8+f~)KB@zHxsaTqv-SM@aty!0&h)8kptv0xhq=KDO6 zQN^;&f?_F~NM&!6p0(yKoly~-!TtDoo921{JQ!~XfmW2i*I4ja8Tn757}N%PG2OrS zY61QZNbZmBI4!o6WVUMuI=F>+g*@F~ejn$rzZ0gbbv>GA<)HGbOiY#9lAUtmhfymT zL_gny?L$|P?&mUd=-^{Uyq+Xt;;LCwEYyI}gR0^MY_9ivURK!&Z%|eG_TAzy{=*;o zqKcbXCs&Nqh;v71Nl^85pu1{#}Df^bz>N!;O%cS={GsboNtX1=bqp0a4t7WLkbw*KU}z8 zx4v6=>b%WYxqe0)rnXl4-O@o-!ze>Rz#hHu%_Ifz`gIrfbQg9YYJ~cI-TuA&t9st; z`z?fkb30h?EupJ_U)JyHKXCFT!EI9#>qlOdp?x~|ILcB`z2Lh3f zpsJqW^^(eGXak_06e#e4TX=8I-mAq{_IA)XFjKkJ&6%{{$G2DI5$EY&5qb?yswrO?rqM zTgzeYo7xVMh$TvS&@izV*0V)$l6l^Qq>O?3%BPQV|G^}a0*C&x|ytYxxzD%EI(c zdq_1t&B6@z@s6KlHnE0(Dq16-e1C<_(Q&!XJmK{cOGU!9*`#k@BjvrqcW@G^jU{#Mmj;=#kRz!(Q?!C7jFyk80k& z{>8yWDfMf4zYBGw{9HOJcW>@7yCJhX<|+3UK%{D&oIZJXeLroLjXa|1Lt1RDFEBhDh|*tjcmYE)2T={Fa3% ze+hX-1j#NN*`{~YeEyJrPF6M)nw_0lU2SkMq)^+?jeb#svma_n|?_DbCdjYZEyj6anEX>hJ;wulsAD*cx>mIPmoQO~vxL<+4$ zrPgj1<(R6I@b-MwS0Y(H4igX~FYx%zbJ(bQY()U*4SX*98`9W5J!v(?)~MyA z{)xB6>g-y$L+;7)_h7}Ki0qj~x-*-RNSu`@R_)z}{X?I%o@4TuJxx$gjR;glfQP!@ zEB?@D9}Vi0mw+~I=~7(V?PWlJ=&L2@rlXk=0~RT$TRc1DRVVgvv<$`nkj9+3-1vDO zcC~py&8A`1T-ExAQvTkZY0I7PrZ-jLY6ElpSj5Mk4*Ba*tNkC@wQj0;w>R?gtpmVq z+CivJtWoQVT=soe=dKzsB^%eX+Br6mnvbs%Lewg~{}9iew*1nWt1A7|&x~sE|0_TA=&=dLTX4row?cFy6?u#iXX;#YHLa~jEl^#wy&|G~5dLyIMgKXuZ7grvpM}uHlDE&bRz!`# zJp5K0JfmED{O$Iw7+_i4$&_Y&y<=|2^=Aa+`Ww5uAYnm9$8Ljz(V0Ew3GqB{kM%5e zy!20)py~`Si?=AcU~w1o^t~!3kn!@4Ul&7>UxGFdZGBa~_fsj=SL+o|4}x3)l&KiT zHWgm-b~V)cV7s8vc-Tg8s+)*tOgy$iHtm3lqP$cx?q^E0n^3ozd7UQnwjT^DP?Ip=Zc|yX%~p6Q%k)&eC(0 z)%J_OUi;!WLvL$6RHi^?Xk>(ef#IkUlDGU7w$zWjRbmS*QIJCG1d}fS@1r}jbkvo^ zA_1xyWuw-ltLKi&{ba+H2|Lu1E@gveD=2DHK`RR#E9P`{&}D3q#LN-#=~M!&D;9sbL6dx8yz3~W*b^wC^oK@(v3s1sB>TUg!nxTL(ZuWZoL9Ebw;kb_8KS#V;B z!UsG@1AVzyo?-kck#CC6hdQXD;I05*l>Ij&!?OKUO58@Y@q4PPTTN=7%#Uj4-g4dn zwuyuZvqyJ#6Ee;Fz6B|E33^-X$MlqD<4lPlxNV;m=p%@smQb~`F?@mbjMOI*jfWE> z%OWhFVHWZBVLb8Zd&^e=cu`?zm-#xkP=Jhaa_E=*do6%}lQh67W_!EG&=J+tnUFPl zA1At!^|vBIo0>GGUP|7hxvOD&!EM3(NOFiy-k?r(*$ep?uCW1YJ|jo1VDrZ&Rk;=C zI@5o|A)qVx?xa}0gj+QmC}3npokw|Ox>E(4=osHL&xm9Jvj{}lW;? zPdBU6&)H1mK5#ek=Jm2u5aIwPUk!U?X~lc%5J6PoJ(}R=%I|!2Y^W6`HCTTj7H?t* zaaa6M&PrmrB#cHp4`>?mZ4A;#78|d`8Xc=nG7~jqTBd|+bcgti+8Qu1RV@U z`e#`s1!|=0q-i}I8@d`F9m=q?UX_nSd5r)3-U-fj;AiY+!rYN|%#vmONJRUM)1dAfEt6RTg&*r-{L@9aLTU`uk>4X0+wVXNBoVBugyOL~04 zVw5+oh~|h{0cjkCE{Fx3iJ7_X?DR+uSY)*PH4PWnwv(}vimTMTVkF%oo|-J0vAKpW z@g@%^Nx^)bJ1q2Ph0g^w`DNuj)fuy`npV3|sHFAK9#BhMUOyaaIss=A?obe%!;e!8 z*X)QnFn0Hhzzvd~TQ-qAE9}TA(j|KLY7aiZ$VFN<^bkuf5&s9Q*ht%5B_f;rS6~Lp z6Up!GwfKNPri+?6EVT99m4VGtrVVyUuk5@#4cBJj)v_)dbErUV;fbA+wu$;tJKV

`IFPYHfaS7A8ra9IoG7nM&FQf50 z?bQ>Fs}tT!ib$o-C*k1m5?_=^!s~XCsf{e&ZwzTJPd5rG5FPM)^v$*#;49x}=l%@1 z>Q~^!cggO=&6=B9P7D+M+~ES_>%u+qcFrvGS%rcz&Cpu9mXjxl8`@hrdXa}M(4uc( z@R*RlpCmYA8+7_oeRf>@=$WoPha6ivcv)?N_Js)3@XsNG&K35#91ZWQIUbzs%#IgP|4(L;N9f9NgJn>5VI*DWrVT3S zIiT7ELEuq2Fb3r3TcOT)UNfj0a4-JFCew)(@=}N0lp8p*kl8SIe<{W_=@uOLI;uTZF9>Gdz~X$S6`f`D$f{cx2%aFtL=e1Q z063A()Y^)Ec$kF~6jG94#gi2BfDGf0l5n{d9}ogRc^VRZb0?XycEsvtFq|*_{w9Rj z-*XezUqL5n`Tob~s3>andq023oK46?zQ;+~>1IWCtx!BClhEe3!`xeg&I|81!qWvF zB%2V9C7o%xdbfX6X)M_@&$4>DU8~^MK1Fpf04?HV8q1&hlA0Ex#$~uFAgDsLS7k1t z(KB4|_^e(-!fp2E*{NS!NcD7a&noFpl{kBgdDtT?FSPE?_a1OxBtA*7yq5&DlC{*Q zTLS?Dz?ZPXABN1dhDb|EAu1k6cXdz-*BnA7igJjot;&x@LDU;Be3v=|mkATgh*MH^ zc?DHhc3GRPkQRR%Fmu_Lz<>i|g>2{iGb#P&0U+;y7e={^J+Q6}R>V6Vq=~EQv<;U# zI*;T;5UP#B%-(3+NXK^gW zg3K4eeCes{>ZyWiAd#{|H$1K)tCX&d+S)v~5Q$UQP%<(5UI&$!NC#1?%QZXt3JQd+ zNKK=?At1vCh&f#AWcJofz>NwZd*xzf5<1uL;nRDBm!FK>_KrgFV>!ZYgyan3MHR6A zX8MnpL(XX(b_&eraS~IliSNU?2_2J!ejWO({&o3fz$aN20@+3b7|8bkgNKRBEWR|z z=>^&=;L8TcsCd0PC0Ba_t{^n2mefMo9)J_5OArz7$w`-&{&74t*2$(Pzb7U9znPLX z27f2Ivh}y8Ug`vOoMl;g%7R-(>bD9-jN}a7ntw?79XN5;G^|r0f-{@^;1LEq}ybr8`YZaXNZY2 z#p!*YBBkexV=2uj=jBsdi0(5d&8R!+7zg9a5;z008S^M265QZhdM=&OlyiAI1-L`k zj&qL_b#C@gVc_n}7s8JVYYI1~&SY9F|6J z!`QbZd??Pr4Mkoc#plH$?1(o z3Ui1x^4dHr;FEZk)cn~&b($59u+fy+K96`>SA9&oH8*afKz<(zF+OPM^t?rAEW%^nxIo>U32$DpPdrtE^^4 zNc0~`Uw2?<9vOhK%aT=ZwJgu#IkBG2-OWalmj|~1>JLdJ*jXv@{r|9zMNbfCswJX-_ zPy5kW@s=yo7eoA6hhtNJeuwZ_T}Yc4zOhqdK9xS^ES1q#M7!?s;&0#?e9b(9?X*yG zD#_Jls>IW%1M%-ooq`S!MUmd})z^8whD5Wq0skYDtp=SmB+Q}L)P3iwD?>mE{+O}>LK|n-Al4Jw{ zNunYk83jag&NL{JbI$RofRZHVAW=et}kX9%VQ3|wx^_147^&geIdF#%lTC~WlN_sdzF ziA9FOjZxVO@S8B-nPTM%uLz2bIQt+LEWqW;l0+{QJ%A>RqfY_F3S|%Jt>3|w7I3*T z_+7GJq{7*q(e&Iv1C70s_7zQmU92tXd8z1EJ8L-KxVCFwJTP*9Ag*1OB+mU9&F%4C z*+ic#_|f5&TA%$1V;a7RQq!H0>Te&aniyO?*r}m9`I2Js4OW5D*yoq8+d9h`oZVIe zS#nUtr2|xP31(>akJ3`ckqpt?(h06_4MzEQ5V_RV*aym8sau=-`@0ks)#-{Q636kH z_(7=M&6@~(%UNTO7|fM~{|yT1+>^}sjg-+OhX^^iB9E1_lCYO7%f)i-NCjE5GZ4=hdw=d+%n z(K>nxIJ*ti%2@eXO2@}li%nwgEbYH|TOeEEa{IZ^a)yDoNpVsEd0a%LS>%1j`PhmH zQ)0D$RZ?pEQo0ZpXz~8dF)noK(bDp!&M*DRZ~E6V2H73T6+MT=jTMak*lXZc+?g@$ zf82F}g3WZNLw<(CUm_F|3w}*!_`9c?-&7Se<(0{IMvk&iBAgQ5Mq0j568gA^&+!@| zw4dQY#^=x_I#ra)wmrJm`HqRe-QeqL4c(QbQC&q+&`xU~LN}m(F)+lXeFY^z83QF@ z@Oh>rQZ5MPM? zC;YEIqY!mpOg|Z-h2$13i?;f>PbW3tq@pD}u<67*wA4+J0o&$Q!di`mlpw%>gPj4h5u4#A~SHNGiluj2p2NY zU@7r@vbql57_2tXYBZtJ7*fZCzT+GX9Py3(opylZT(JOHhk6_A^DzA6{h97FEXZ-s zKy%S{XkN8y{257n=ooVs?whGzAM;>Y`tzV9d~|J7*boo{^Pd0dXPmlv7Ms0aVErB} zJ-X?p`QHNZ?LmqS$i7zn-vAC#0Q8PcO7H=A$$HNr6xriq|>2oW&Uwxa$A*B5D9)Gf?lz_c$T_ zz2~v<72tAQ!gtYs|ID?(XNIkIFa3TwazF|H=Js3Rl#`1Qdtv*c8f3Wbk6WUyx62xw6CZ+_zsn#D{mGd*wVXw482Jzw5=vGCreEIzjX^w1@>-P>lYW1){S zAc`nLS>>W8l;=;6N?x8z2LnNs>yPOJdcd>#ZIRZ!)MRPb^J^vAx|RRPLLDgU;bF>ZGarkr+3N26|1L#&OH(IHgj#3ZIO zMn;`IlDH*K_v(IOezen%A<6c}r%$lt-ebF$6NFeh8Srf5Q}`yyot{nI zvg03Hc$|FHCm58-Ym{oG(M#;Y_JHeT(|3uwvc=6+QguiMm)@&|&+7MB4zuJl6v2e= zM`zi+^*WR;Z;zVmlg(Yp z3%eTYscEyW&l@b|L4TV7J0r2f;M;)TR?eTk`_Ld!L6AfZP>BY_FZO)-jN@?HO1<`> zTFaQa)vRo-=XA1c<4db{*Wt(g9!-Mcec@QlOzQjh%GX&oFntB4dA8WtVCEECzLy;) zkYa`rqaH6vH*>y7N`Ns_E{G2E&J-GXd>j6&>l5uFpeXTYO+LFvbe_FPxK%#u!OC~q zJGW%i6l)?dAN?#gfNO7j%2gTJMqj|S5Hx$3IaK2p{Yo0|A)P1S+b~q0>@(plTMakU z^RTAAvu(i9?$3RFX@%BEXi?cl?`=q!m#3qB7T3j*rZ;G4_}V` zQTwT2a;Mm!8y&(Oz|5j~?K>c}@N*;x*-s2tl8=EfsvM;x(sPs$1@WQ+%;GJ8xX3?o zoE~iZ?}{sQ#eWl5xSRcv#o2X|-^Rw7NVa)+y-g|U1`vy7rB`pKA`8^!3<|1NXCwT- zo=#3qrs;*H>c}E@m#!n;3v$}d`m>>%$GrbhpiAX)M-LE|BB!&sSv5 zOni}rN5`L+vIHWVCFgNCrVfUC0->NsHM~VM2nTnO0(s_ByN{5V;pV@FMH+PSEC{ijwiSt`#%2_R;US!oY&Oe8 zkJ=dMqt5{pV?pu0ul7%~>KngwQf}yj7xAq?i2<;6oHXYPRkQVUjd!?_$S^M*=@y70 zxJ+o$;M~_w`>}WI-axo32ilY@1jS0Z;=CL)@~6N?fm~>c=ZFyyvk^}FNS|J(5cw;Y z_iA5%@jS0%H8reQ&aeS)UkDLCYfKVtp*^^KvLl!1$}mm5!%M1=#eExDu1LihNpE#b zy;UIObwsVU60y@Jxy?&HbD?B0UUn&a!h+ydNo-K>*%-|ztW4V`|Q(=9sn#i#GSzu5ZwT*8!|CXYL2N*>c-=xYq?^+XR z2AMO6>qZkU4rh1@HsUU4bRH5f^yrITq$@Q5s1)lo)jQ;wtTo~jR7jUqO#VX(f#ALy*XUK|NtSymyoz)e)q@`LSri#&&Nj-V_{ani3-I+9{eo3LnB)|y&sz98EC2J(+-nXYrglsV|HZP?l$$@lsMy;X>ys6MEh z4EkJoj`N>H0^2>o6hi4$7{GxeG|4ql!Ng!v9QU=AqU#^h&5y0|t}*l7rt{(_TZai* zEEg6oNdKT+Gvb*5u1g#^vr=Ybr4{OFN6h^NIEU;KM^}0b|EoV@HB+Lvm&mq)(6pwgjNn z3ngLEO{3lL(72KOF?K8rO;D3!uTf5EF_;i&9${sL94HP~T;z{}3V3??xT`8%NTd}I zL+El+x*8QzDlS1EWO1EOCivxCc+blB55cboq9Ln*6=>P8{tk58((Kfy(03P*IiBE# zQoA%7C692VWNoWBJFY|~buRYY44Fdh1gv{MjT=rrpM2`@=xLC1a|&8oT41W-f!Twt z+0f8XC(CPw#kS&dp2Z2rOzoOKxcnfUF+&qzBfT^0h7qgLZ$ib00v!9w_ga~WHa|Mf z)shFV&=0oXP&dUYjB3>JMi<|o>MF^bRgcyp?FU9!z`@t0I8cZ5;@JTTi0oB=a}wU5 z2go1!u~t146$7+h-~m=mg58t`=Q(@_a_%P)K>i_O!5Dyo8$d3*cxJQe@-P~_@X=|e z<$+A~Mrk4|Ds}@rbOK=1?Bdx^RJoWKdyAa8;RYOwZli7$=o9gmBjEqg&4MURIa!-r zF6PhIf%XkL)vWxO7_L8y0%UGumL|n}V>{2&3O568?SQCiu3je> ztQ)I0 zl)6+$VR{}}fE@C#{ zQ74a*%;#S}w*Te}jlb4^$|f3xiTh#1akF|&{ZD^pZT4rP@ZPHzB=VQg%7CSA4v`-t z{in4e3iwL?J7!G1sK1#pJye{Xk31msmzW#$K$E;u=j^C1qwrVF&&7fX}-L>FaP+6mB zheEyTnQe1*bVxdr!xGC5#orxV##niB^WYT*U=A}gH3igesu~)If755nir{wq{p61W z!RL(~RR2R`hxyFJM@yLm21P-tT^-zJ9&AGGGMJv+p56v)lR|RiUPS}9I9&XQixqj_hvKpf2k4vOs7V%+DVy@x)Jh&gX|j3-DA7`_j#8lE2ZVmYNlx_2s@kajfLQ zofJral8Dkaahs%HIL&~9C11Piem)hNfdYfw)_5QID|fJPMS zogmtZP6`$}T=!dj$Mb4m{rHs8-8pHef>kp~M`L|ZtVHbTy6jIeQ-Hw$e&^7kL)FPq zb@-&yOT#HbZLsyRmDt5mW^gq30!puI!E`;4=)HHgRM@1LeH$SMV#r}=Wzlr*>upOd zAiC#7Z%^3#>7yW3Rm&}s!TN_9kip_~|63^Bs_R3dUhrl%G=6xnX81&M>$y!lx}u!b z6H=|j&lgIH!hrRK(hD<~?q7AIDrIcHu}CJ9`OQH>WbR6=-~OEE*ycR?FU6l~!E=+% zp7r?UUEk~iM1hALBIP@h!dkAr)OX5P7s@Qe3N({?W#(vroY3ce-En$?o{Ssa@cOid zEN;AEgIsojL9Z9pDWa|wAYc>B>nWWoCm>i(xmpH-cR0%FG2Ba@tOX=zc2P0`b@9o` z(`+Ngj4l^eFSldQd982okZurs{zrtukM|^lRx%Z*pTyo|+>;5wH ztg8tO={E!?eChlCOo#DJOAHuqY&+#bm=K;OMt7#!A?{h4VFfa`z7h?qtI&vA+ff=L z4USb5XkAJ;b6O@GHRXBT^r6yuea*~S4}n=|i0W(LGX_W$CSZtOl;sPE_&1j>0YHHA zB|!|z&34A#Te|^&E?*SCVAlqsf65i0sO{h_^YVhY3qTchFd({2BvIF(VBZ<3f8&f# zzs%)xL0o-?d&?PxO`Y64cTq><8R5?B4N$+B-_bLF7P-%Vj3)s}-~afg?O~VffO{UV zy_iSmUCg6--7qqaeiq0pG6G-zDcQr$IG5~UTs&$#t$`EpWP0wpGbYfE^_Q2SK`=D5 zXRP~yiAhrJ!{K7>fnVJhLMM*Mue;&o#Fcz;VJ4TmZTf}x*rTQf0^wJ)A2GDMfR%3^ z+yQorCze4|#SYqGt>w(+^25hK5^XNO`{DE4S~#hV0BG%oCD3<4x3k$ERRmvlAlcoA zd^m`#RB?zL4UMjm^oKKPkrA}7#@StHf>DMw1|~^1~h#zHsPpeZ9(3>FbwE{eW6IUs6Y-%+R)_-K(wGo)@vG z_Km^fBY~NLx|I0ERWlA?)d?A!-U<<%@kLd~m#^1am^K;<9%sssXLgoW9V@>OZs;n& zwCWx$UxxJrnJ9&q6E5#kolw2s9K`0vt6yTQ(SGN$v4^Xx+D7${whDX5F$l@$&-q8a zvzYD=?q=&*$$Y7N{#1)f(_ldKag zXr_EGmZMinRvl!Ql)dR&sVMg@Tocp@s9}MN3VLkhFr73GG8a3)T=1#DR`Lt_ zegGqRnCSZUJ{c`|^5fRFH$IOpYgoF$Q%_4CzW4!HqRYn%VKF5Z7#h}zp604Wbo0kh zMI&kdH?b%ClVNa{%$hawOm4p;2+`u&@(kbejzie~a0g$B5n@AJ>)6s~uo2x47h9gX z_STcT`bGMlZ8I&hX4At{YRA()heKn-`)OLu$+6CxkfNB$b*i+*T27A!-(!q7cwMr| zds}`XgA|gS*;-()M;CIi$5Oz!5Djf*%YL*80f7jFO9SjlHQ4O?#$iW|Vw^J~{RwXX zcO(M~w@O?$JiNB_fCqhkgwcsOqu(JsJpey^=7c&Fhb2FYk-?LruXu~mD2iA&G+oz8 z-gWz0+|AP^@BKx=!gWPtKizzIz1zC#a>&Jr#$+VZ51E6=73y`+^M@xvBeix*W!(!> zii&dyY2`VM*&{`-Nw0>tRtUbD-nC!;&PY0JN@Yh2(}nzykNcGF_|CH%dh68KP<}do zc_dSNHnif0$Rg{#lEZdqOd%fUdu*_jupzeFB)6RSm%!?97MFb9lR2l8AvWzvp~C_X zulr6DyOw5*-=G)25IJup$byRkr1M%uX*jPIMY9X0%tX=`o|%)68^OsjobJ~4F*+U9 z-QUyaVv+lu>EGXQ(CJd8xLFRZASoXih#FUIW#Ki*4Zi2##*efROeEuc;Qkr;NC62w zVGyL_u03U+eP3AQ*c9^J1(VLQbrNB9)ojD-$ge!TT-U?S*8lKcG=pOE z&kXGvKYKyZ@Mf@MgSuMHqvUoQx_jQMg{pDjmK5bp33F;i7o8$prxfTj`|#MP!Pz(1 zbjT?uRLdPfT?CI{u17Kaf5CtfkIS;K(Nn62^~5*71ZJ}ClvPx(Yz7H!BURJQOaba*d8j2Y?Gra5e~6O_4xJ{ zCP05uYKC@8<)(3wa28csP~@OYzO2}V++FiI@I6P_q+eVPKx`)hCJAZcCx7}qh(`gF zKFmnPU4Nq#&@~l+wM(=`N_MUoJHryjq`;`Y|I6RF3In5K5!W{W!?2x-eerge{_SgR ztnGP=xgCvH-~4C-fx*M(Da!V=zL24}KUN%QsQxl14Yzw==73;N!@1-~#c)@wHP1RPt74Ia&$q^8^>xs|Kyo2fERq&Gj^v#pS zk_yIjjlXec+U8y5!V~xc!6<`rSjOMn6Djg={b~<6cVwz8#8Mq5=;#*a+ zLU51i^IfRmo#&KWiJanAOp%iLRHT$u)9H6Cq>T`guzTC3$gH7{bMNWpf^OfgK0ZqQ z1`3EWAc^4aS?LfO4kSx)B&s}lQ9BqbkFu6b-v#bowy0OOutT?kgnE)tz`ajMPmN45 z^SbJ=w#|w8N3ZYe8<=a-0-L>#I=hi<_Bc9=LqDjZ@Wvyfp z>}_bk;*xe1J0`Wx!}F&URCqS0zB0s2(yZxR(q>|5W29!`FT2)S=5;A+sLhl&tpxuqe27 zUEMkg*9-)%+Si79>7VZRog%R!w76qzq3X=0RPCJCcW%LH{hXWO50H5B;$eP}3wo}O-y<{b%f+-=Gv+Z>?}_XurF?7x46dNN?t5V6egX3T)t;aUjv z8d`~1D11nOh!C<}-+?VP^x^hKK36sBP_t$LcAe3vg`bX@OU7eMo2C-$I%|#PYCnO5O zW=TxzpOvQaIDp0n203*zip^OhzP=JcyNZ5QFbxlwL0Jzs8{(%=CT(jSy%!eaK2^vO zW)ys+XVbFBF+3O-RRGs=Sb11flsCOJ3HDlx+&^SVtMOVt-LF?;i~o2*{=s(s%53DV4L9&xELN`ndoNov@`?DRT6!T{t{cKAuWwW!~FyVP}1dP z29K=4qOe%*{erY(ueQ^%;1(adD^lT&pg3H{*2z502@bH{BewSa+rB+R8z zUmc+n@g(o4G&+82r{Jwn94X;}p=%k_u=8EZgB~87Ypcg)K0~3rk}!ft5VnJYPQ5av zXsSVG^-h0PWbGm1n91k8R&X4^-Z2KRh0Cpx1t@{{#P0k`ZW2DW%Bp$&hch zbFtefW5^7+VK6J?X4Ji(z(boD?AyY>54~}`h|Y9}d0K+~M7Qik?tTFgo*91c^g@63 z8@p&+`2NZ~Nm&{{fMs-!tCMmc34paSlH(K_UMmmSZgtj)Fmd%(#hSgs8-aLLSsElgq zU`z?@0CP&G=cPD4%yzHZO5TY~j+WU0ygrkxxYfWI;B{;5XIM1Rbx(0Ho_CMcG zymX0JLqhbavTd{Nto#5<_Gjz>yjar%C+3U^LB)M*C=h@4SzfqZz)}Ei8a%^I|F66l zwo!g%l&*&X9RNv9!Ey(C>zVzTc0A3SBB29dFJW#caEEJVo3?wG@%A3>l6_VwJdR6p zzL$C*`#|jVl@VjltOL#3uP+~Xs=f4Vs4w1_uB+9da>@_AkcHKi0kNUXqC!syO=V&2 z;T8DCdRK3zyQ{dilkQ1FMl5f9Z=B0istryg1NXKQFzN5gVteUAXPbZT(GvWqXrr)_ zYbZKC6=dFa+|| z(z(hve0aFi*R|%qJfmmFlUc)8{9z#1wmx~}I-12^ijMUl!XQ!LC=9Aa2Pdlra7D|P zm=I*iU2t_3FGq1zQQpf5<>ka^1`w;+7gCZ7XjvW@v2{QL^be3It&Zv*4)WldnH^R$ zSc{_^SI`*8;}M@}#)Pym(JTa&A`BEGM;^1UELY4bu53$uy@RveX?wiC{1tDud7mKi zDn>VNsG1!*qiMdJzrt8;@xl^YMm;$~oR&5^nS*RWta6vKe8T+xc&V|5<5QQ`LAo>% zl7z?ULCdV=#wFAl#c|PL-}pmuZeS}fuI20m`twoX-+IZ_PLwAr5}~Fo6u)cu_EF8p zAk4lYM$tTh1oq5_rowq;Z^;pue~3hpL`XPfvJENfKBo#q`6ph6pbg$dr^**rQBeUJ zQp3i|+uPbKC#w;_*d|bos{``%WS=9Tt!U>*h$IKMk$dfy!4~w@n&g|IG1v@;V7}u1 zRMW;!Xl66BV&=u6szzb~BF>6pWUv&l`ks2)ONNGRG1*%wDJt4YZ3QOAt4=m}mPmY%HW(o-L=QE*KjAuK zz6URQ6p%ZVrJxZ-vSZi#OnRd(AH0#PbSqaWtB|`rZ%*M5pjK) zv8s9DcjLkdz5d3b06g!5+x~bW0XLqVk74rOx%Q$9Vy`+WiH3KtgnU8**J5ti+nDn~ zxZ39io>1B^3W_7CcT9~Lp;|o8;*JZkc1-n>NaN?LU+8+#&eW?bnZV1%r*73ywyOl> zcEg-O4KGr)gAyC=aYxpPqDRq|5PXWzoZR&^Jp`XDRxNMx%^sPy-~6%kmE+w^D%eEd zgBPSc=}^V+mgPa-Evwn#VLX^9jj6#XZp%Q|XL=-kiJ64V;d$%573|Ho(l9)eN5M2- zoS`-^T62u(XbGO9vj%VDOKoq)%4-liE;J-%ESroiq#v)A5hCNN6cMt`*XA9?;U4nB za-2H=43p4w(hAFG;g;x%!(W{@&A09cjaMlsKbVWeEDvYtN<_@O9;$5_-t}}HKY>@v z3OqyFwW}2)b%5qeAF#XQi#Fd~Zs&5>7$0tWeClSCk2Rk(IPBisU5tEzKb7N@k%Los zm?JY*7C7igh{BE~CO{U+RU;sszl=5?xjhl~Z&{k%oxLNo1)3B#I=0Pz9vFnjh=2qc z0<<`cW9gYtIw5)zQbFlg?tps)O0HCsBw$b0z8hR7ozW^VN1e!#yvpcdwsp5-j~H=S zzidxSTXp*((_M`-=CSO0l zQ*RI!uqGhi85lR*7V@q}e&y!45gduk+3&zRx!?)^uhX2T2*hm1&WAbD8rwqxX} z*$V^@dt9{Mlv8pwUWb{xksXTYX7~zea@5Rx7KK9`XPovXkB)W+#6nY(ds?JgTYL2i z^%8jdsh&92;Kq*fY@ybS_X$>oKW6UtZu8h|I;_v#+xXqBdGcY-ShX**WD~F9;dxoN zbFc{EFiuePd>(Dx?Gk!A1vomv-ZpHtup}JRCs`jo)nl@p)TFtblZJy0+?hKCK zOx9+{42w@bZ{mFP7FkjlujL6Zn_^g89ItUt$_+`=g1c&JYV#zBJc`(|7JvKm(f}a0 znkuyevWUq>O<$2H1jC1M5rSL+?hI;$X4TT??YWkdE(=*$E3C2)!Ls7_OY#h2%SXI% zP}s;q5uk%)EOEl0O(&td*500Igfd;85P-|lokJxbE>>aVVP9^)>^nXDmnV=3wb?Fg zpUT0?^-|Lv0iFDN!E`i)OmvjC80*To9uI zR2PL9FtG7|cC$T`?}xkkC?)FLCtt1|ytUe}T{&W^ORI5ORcm zoVpd4in-G${aON^m~YQ3XQkU6>~xK+RV6l{n^b19_HyF7bXh2aHi|$EJV+sIys#k!GWIUmH|ojelA1Ad%> zg<=+cd;x@g_F)9DKXXfX)>^I=<5NRxnO_gDy@WHg{wLCkh-h82gcTwz_?#emh_T^w z<$S(uID)spWTjDQ)DU!`@_Z+@vqCs>K^@Ok%i7&mAF$vVWz|XemHC1*xD~@W@I$!QB?==1a2%(9uDfgS)R&-1 z%WJ)WY9x}-a8X%I=iqT=`LTb*$8SMQJhqe}E!YvFgOak(kF{#r0i}XPu`MFmc*wd9{*r##Pf@Ac4ir{)g;3 zQ{mguEY$ku$#<)+Z8J|SvxLZ=1ErhRwr82)@s(-gcHQc!gZaH-Txu}}b5+_W`1J_s z&#Y4S1uKnA`pCQO?UR%4zxRJ3v5gx+)CP)(d#3Ozsiiv(pYP!#+h@{ixe6?Imp^~n zvy=eGwP+u;IX~ZJ&QF4J&{GPSWZ_DFhg>#v9Mz!jECC6L<)pZKqC z`>{%8++;cMm$0ND>^#^9tF^(@&GU!>ETDu@p8Zq4o|Sf%44VLs%y$8H;QgpP-G?xP zsRuPY<5S=#Msg%m5A{A-L7~9ZF@bh3q0wz8oV?4w7p_*;>G*m@133}1FtgM-4(|4W zY4xuj)#I!z0qR|k-gDGnjhBjo3kdixv2L}XRDdtuI=W*~h^IiZ{EBY>6W~Z}W%lXM zXG-o=diIqQzfy1us5Z{MP^NOrsYptSIp}=R4SDv+z`v^5!}Q0ytIfB*YSm}|Simix zXGWova2#uq%(L}nidf-bZVVfBx-qdg;w~ZiA;E4}wX_0hJ5qSvW+&aopq zqx?ZZC;dNBN*jl~{x#^LF1FWWj(eM(yXH#yTQ9~pKiLrzj-DFu=elw=7ins-FANNFOIItFcj8^o`75b@3Um?4cvsiT&L7bLTYbGnO9wo!H^6L=ii> z@(Z+{IkE{kl`FgPa#=UJKwJY;xzW1cgSQU?#}5vnfoevqGIrD7;i}EHk&FyoSIok; zRjImY9SE$*%{{X`A^A?DgUg663|mzAk!6@x1c6}sQij0rR~kWES|ImQxwl*vCY#7h zL`8KvZD`2V-YtbMB*Lg8rqlWOL4v`_)f|l){gE=J*MrM*g7L%4U|6+`j11*tV|h&a zQnj$$+}vY6kNcbbVK=7Tgx`Tsy z(b;c6?tJ}X=m)xTvI`pSno5JfAQY)JDm*?N{Wvk`&QEr zipM|L8?zD+5GaLNa_s2Q{<@U+>>2Ubr85ebq;i=Hjpw$~sdXR8SNl2#zBV^M9~m9J zYR{Xi#)RUWjPZT;*!pl1X{@c#W^Rj1$qB#*-T@PwKvlqiqGaF^PH6d+m@1*oFpTBp zu@;Z4!C+vEl9rNeiASWrzCvJbH^9&3Qog3o`=)~quyZnzk_RTiO=Qc?xlm5R#B z=|lJdw@BSI$aVHzs>I-H+Ky!HdtPs18{#$#E%!0RNw z*{c~DHqawmZTdB_CE%mn-O}KtxSsrxTJWeaE=(H)5_{fYMf+a9i?(PrCCdMrX`*T( z>znhH5KLEx8yj34y;pjR?UFpQfuvxD-4lO3JNKrVm=0l%4P|d)jd^|*ct+)}gHb65 zFC46SKZ@?rUeUdU^L`uO-vP@3WFG!nR`GNP%5~|cO``u)$xT^-{$f@eX*5ibN@5~W zd@ci96#)^2{W9x$12SVyATdx)bK3S9gq{xe$R1A=U^tj!18?TVKhUXig@^MqfsvD@ zeU?QHs35fU{H85r0I&*%`73pZAKS3&@xbKV@vrzpj|L+Ho^7eVbzBLh#B_4p_vtB9 znUHj_12;LO{xJa##zwt?D)0uKmO-;}(m|ISl22C@OUt9g9Y;-)X6>HzTpI)v7UJi+?bOo$tNS~d@K+{>;LDReNOmE~^>8<35> zQ&$`<=TBUm4PkOr)gMTa?fg8y;yBixifTmrlH_c!70LwjNNlOqmOa=ShLSC3lZL8= zYd)PTjnf}3@-E9V#Tu?N)6&7PJLlfk6uhTOi^jJ#AZBbeaT!x6sPl57>Jm+WeXW6UXcF0Q z_9e|vJ1$@Jel6PRuh(vl3aIP4T2*=fBz9f#2F%USqI?nzYQYy Date: Wed, 10 Jul 2013 14:32:10 +0100 Subject: [PATCH 23/48] Fixing up content negotiation docs --- docs/api-guide/content-negotiation.md | 29 +++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md index 10288c94d..2a7742786 100644 --- a/docs/api-guide/content-negotiation.md +++ b/docs/api-guide/content-negotiation.md @@ -43,7 +43,11 @@ This is a valid approach as the HTTP spec deliberately underspecifies how a serv It's unlikely that you'll want to provide a custom content negotiation scheme for REST framework, but you can do so if needed. To implement a custom content negotiation scheme override `BaseContentNegotiation`. -REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods. +REST framework's content negotiation classes handle selection of both the appropriate parser for the request, and the appropriate renderer for the response, so you should implement both the `.select_parser(request, parsers)` and `.select_renderer(request, renderers, format_suffix)` methods. + +The `select_parser()` method should return one of the parser instances from the list of available parsers, or `None` if none of the parsers can handle the incoming request. + +The `select_renderer()` method should return a two-tuple of (renderer instance, media type), or raise a `NotAcceptable` exception. ## Example @@ -61,6 +65,27 @@ request when selecting the appropriate parser or renderer. """ Select the first renderer in the `.renderer_classes` list. """ - return renderers[0] + return (renderers[0], renderers[0].media_type) + +## Setting the content negotiation + +The default content negotiation class may be set globally, using the `DEFAULT_CONTENT_NEGOTIATION_CLASS` setting. For example, the following settings would use our example `IgnoreClientContentNegotiation` class. + + REST_FRAMEWORK = { + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'myapp.negotiation.IgnoreClientContentNegotiation', + } + +You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views. + + class NoNegotiationView(APIView): + """ + An example view that does not perform content negotiation. + """ + content_negotiation_class = IgnoreClientContentNegotiation + + def get(self, request, format=None): + return Response({ + 'accepted media type': request.accepted_renderer.media_type + }) [accept-header]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html From 453b7b03dd5ee43cd4e63d05258116fd9f005fb3 Mon Sep 17 00:00:00 2001 From: Matthias Jacob Date: Wed, 10 Jul 2013 19:02:38 +0200 Subject: [PATCH 24/48] Fix broken links in views doc --- docs/api-guide/views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 37ebd55f5..683222d16 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -137,11 +137,11 @@ The core of this functionality is the `api_view` decorator, which takes a list o return Response({"message": "Hello, world!"}) -This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings). +This view will use the default renderers, parsers, authentication classes etc specified in the [settings]. ## API policy decorators -To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: +To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: from rest_framework.decorators import api_view, throttle_classes from rest_framework.throttling import UserRateThrottle From 1cfb1cf45861e7dd20b5ef3eecb6c0402e15a82f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Jul 2013 21:14:18 +0100 Subject: [PATCH 25/48] Added @cyroxx for work on #978 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 42b28d263..381463276 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -146,6 +146,7 @@ The following people have helped make REST framework great. * Igor Kalat - [trwired] * Rudolf Olah - [omouse] * Gertjan Oude Lohuis - [gertjanol] +* Matthias Jacob - [cyroxx] Many thanks to everyone who's contributed to the project. @@ -328,3 +329,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [trwired]: https://github.com/trwired [omouse]: https://github.com/omouse [gertjanol]: https://github.com/gertjanol +[cyroxx]: https://github.com/cyroxx From 6de9b7c8caaea00df2b1399ecd7b815ac556a70e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Jul 2013 21:02:47 +0100 Subject: [PATCH 26/48] Add note on ModelSerializer validation of both model fields and serializer fields. Refs #949 --- docs/api-guide/serializers.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 8e9de10e0..a1f0853e3 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -308,6 +308,12 @@ By default, all the model fields on the class will be mapped to corresponding se Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Other models fields will be mapped to a corresponding serializer field. +--- + +**Note**: When validation is applied to a `ModelSerializer`, both the serializer fields, and their corresponding model fields must correctly validate. If you have optional fields on your model, make sure to correctly set `blank=True` on the model field, as well as setting `required=False` on the serializer field. + +--- + ## Specifying which fields should be included If you only want a subset of the default fields to be used in a model serializer, you can do so using `fields` or `exclude` options, just as you would with a `ModelForm`. From 77dd334026cb2f8a45b817c4577f5d5ea6a2671a Mon Sep 17 00:00:00 2001 From: Yuri Prezument Date: Sun, 14 Jul 2013 19:16:56 +0300 Subject: [PATCH 27/48] Fix Mixin class name in viewsets docs example --- docs/api-guide/viewsets.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 25d11bfb5..17c528b33 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -205,9 +205,9 @@ You may need to provide custom `ViewSet` classes that do not have the full set o To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions: - class CreateListRetrieveViewSet(mixins.CreateMixin, - mixins.ListMixin, - mixins.RetrieveMixin, + class CreateListRetrieveViewSet(mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, viewsets.GenericViewSet): """ A viewset that provides `retrieve`, `update`, and `list` actions. From ae63c49777f4d5b766b85a4b28f6328bd6f9516a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Jul 2013 11:38:38 +0100 Subject: [PATCH 28/48] Added test case classes --- docs/api-guide/testing.md | 30 ++++++++++++++++++++++++++++++ rest_framework/test.py | 17 +++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index aba9283e0..40b077633 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -167,6 +167,36 @@ As usual CSRF validation will only apply to any session authenticated views. Th --- +# Test cases + +REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`. + +* `APISimpleTestCase` +* `APITransactionTestCase` +* `APITestCase` +* `APILiveServerTestCase` + +## Example + +You can use any of REST framework's test case classes as you would for the regular Django test case classes. The `self.client` attribute will be an `APIClient` instance. + + from django.core.urlresolvers import reverse + from rest_framework import status + from rest_framework.test import APITestCase + + class AccountTests(APITestCase): + def test_create_account(self): + """ + Ensure we can create a new account object. + """ + url = reverse('account-list') + data = {'name': 'DabApps'} + response = self.client.post(url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(response.data, data) + +--- + # Testing responses ## Checking the response data diff --git a/rest_framework/test.py b/rest_framework/test.py index 29d017ee4..ed436976a 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals 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 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 @@ -137,3 +138,19 @@ class APIClient(APIRequestFactory, DjangoClient): # Ensure that any credentials set get added to every request. kwargs.update(self._credentials) return super(APIClient, self).request(**kwargs) + + +class APISimpleTestCase(testcases.SimpleTestCase): + client_class = APIClient + + +class APITransactionTestCase(testcases.TransactionTestCase): + client_class = APIClient + + +class APITestCase(testcases.TestCase): + client_class = APIClient + + +class APILiveServerTestCase(testcases.LiveServerTestCase): + client_class = APIClient From 3eaad89c160c478e3706206535929ad0658ae8c5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Jul 2013 21:23:34 +0100 Subject: [PATCH 29/48] Docs fix --- docs/api-guide/viewsets.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 17c528b33..47e59e2b2 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -99,7 +99,8 @@ For example: from django.contrib.auth.models import User from rest_framework import viewsets from rest_framework.decorators import action - from myapp.serializers import UserSerializer + from rest_framework.response import Response + from myapp.serializers import UserSerializer, PasswordSerializer class UserViewSet(viewsets.ModelViewSet): """ @@ -176,7 +177,7 @@ Note that you can use any of the standard attributes or method overrides provide permission_classes = [IsAccountAdminOrReadOnly] def get_queryset(self): - return request.user.accounts.all() + return self.request.user.accounts.all() Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. From 82145e2b06e402c9740ee970c74456a59683667a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Jul 2013 21:54:13 +0100 Subject: [PATCH 30/48] Only include APISimpleTestCase and APILiveServerTestCase from django 1.4 onwards --- rest_framework/test.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index ed436976a..a18f5a293 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -3,6 +3,7 @@ # Note that we import as `DjangoRequestFactory` and `DjangoClient` in order # to make it harder for the user to import the wrong thing without realizing. from __future__ import unicode_literals +import django from django.conf import settings from django.test.client import Client as DjangoClient from django.test.client import ClientHandler @@ -140,10 +141,6 @@ class APIClient(APIRequestFactory, DjangoClient): return super(APIClient, self).request(**kwargs) -class APISimpleTestCase(testcases.SimpleTestCase): - client_class = APIClient - - class APITransactionTestCase(testcases.TransactionTestCase): client_class = APIClient @@ -152,5 +149,9 @@ class APITestCase(testcases.TestCase): client_class = APIClient -class APILiveServerTestCase(testcases.LiveServerTestCase): - client_class = APIClient +if django.VERSION >= (1, 4): + class APISimpleTestCase(testcases.SimpleTestCase): + client_class = APIClient + + class APILiveServerTestCase(testcases.LiveServerTestCase): + client_class = APIClient From b676cce61f364095caa0bdbc46c22cdc87b98646 Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Thu, 18 Jul 2013 19:03:38 -0700 Subject: [PATCH 31/48] pedantic doc updates --- docs/api-guide/viewsets.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 47e59e2b2..0c68afb0b 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -98,6 +98,7 @@ For example: from django.contrib.auth.models import User from rest_framework import viewsets + from rest_framework import status from rest_framework.decorators import action from rest_framework.response import Response from myapp.serializers import UserSerializer, PasswordSerializer From 8f2e71a67fad17a0e44df6747ca450a101979c24 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 19 Jul 2013 19:49:20 -0400 Subject: [PATCH 32/48] Modified documentation for CSRF as mentioned in #984 --- docs/topics/ajax-csrf-cors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index 4566f38b0..0555b84dd 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -23,7 +23,7 @@ To guard against these type of attacks, you need to do two things: If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations. -The Django documentation describes how to [include CSRF tokens in AJAX requests][csrf-ajax]. +In order to make AJAX requests, you need to include CSRF token in the HTTP header, as [described in the Django documentation][csrf-ajax]. ## CORS From 2e18fbe373b3216b451ac163ec822e04b1e76120 Mon Sep 17 00:00:00 2001 From: Pavel Zinovkin Date: Sun, 21 Jul 2013 17:03:58 +0400 Subject: [PATCH 33/48] Updated EmailField error message. This one already available in django translations. https://github.com/django/django/blob/master/django/conf/locale/ru/LC_MESSAGES/django.po#L343 --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6e5ee470a..f99318877 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -512,7 +512,7 @@ class EmailField(CharField): form_field_class = forms.EmailField default_error_messages = { - 'invalid': _('Enter a valid e-mail address.'), + 'invalid': _('Enter a valid email address.'), } default_validators = [validators.validate_email] From b6d6feaa0210bef225f2dad0d5765b32ecfc8cd0 Mon Sep 17 00:00:00 2001 From: Pavel Zinovkin Date: Sun, 21 Jul 2013 22:43:19 +0400 Subject: [PATCH 34/48] Fixed test --- rest_framework/tests/test_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 38acc3540..c24976603 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -494,7 +494,7 @@ class CustomValidationTests(TestCase): } serializer = self.CommentSerializerWithFieldValidator(data=wrong_data) self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'email': ['Enter a valid e-mail address.']}) + self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']}) class PositiveIntegerAsChoiceTests(TestCase): From 61a242e27b483bb9570cc8eec2bc42a81ed09c45 Mon Sep 17 00:00:00 2001 From: Steve Date: Tue, 23 Jul 2013 10:40:54 -0400 Subject: [PATCH 35/48] added script block to list of blocks Phew, was afraid I wouldn't be able to overwrite this section.. --- docs/topics/browsable-api.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index 895b3c5f3..2ae8cadb9 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -75,6 +75,7 @@ All of the blocks available in the browsable API base template that can be used * `branding` - Branding section of the navbar, see [Bootstrap components][bcomponentsnav]. * `breadcrumbs` - Links showing resource nesting, allowing the user to go back up the resources. It's recommended to preserve these, but they can be overridden using the breadcrumbs block. * `footer` - Any copyright notices or similar footer materials can go here (by default right-aligned). +* `script` - JavaScript files for the page. * `style` - CSS stylesheets for the page. * `title` - Title of the page. * `userlinks` - This is a list of links on the right of the header, by default containing login/logout links. To add links instead of replace, use `{{ block.super }}` to preserve the authentication links. @@ -170,4 +171,4 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f [autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/ [django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light [django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install -[autocomplete-image]: ../img/autocomplete.png \ No newline at end of file +[autocomplete-image]: ../img/autocomplete.png From c23412b51c5d8abbe1c103d7e177606644f9f0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Wed, 24 Jul 2013 11:02:10 +0200 Subject: [PATCH 36/48] Add @pzinovkin for #995 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 381463276..f6b59ed4b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -147,6 +147,7 @@ The following people have helped make REST framework great. * Rudolf Olah - [omouse] * Gertjan Oude Lohuis - [gertjanol] * Matthias Jacob - [cyroxx] +* Pavel Zinovkin - [pzinovkin] Many thanks to everyone who's contributed to the project. @@ -330,3 +331,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [omouse]: https://github.com/omouse [gertjanol]: https://github.com/gertjanol [cyroxx]: https://github.com/cyroxx +[pzinovkin]: https://github.com/pzinovkin From 3fbb4097e64592cd02924302fcb4e63dc1b6062d Mon Sep 17 00:00:00 2001 From: Will Kahn-Greene Date: Thu, 25 Jul 2013 12:55:03 -0400 Subject: [PATCH 37/48] Fix *Throttle class names in the docs * AnonThrottle -> AnonRateThrottle * UserThrottle -> UserRateThrottle * ScopedThrottle -> ScopedRateThrottle * (cosmetic) removed one errant whitespace character Fixes #1006. --- docs/api-guide/throttling.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index d6de85ba8..56f32f58a 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -12,7 +12,7 @@ As with permissions, multiple throttles may be used. Your API might have a rest Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due to some services being particularly resource-intensive. -Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day. +Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day. Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth, and a paid data service might want to throttle against a certain number of a records being accessed. @@ -44,7 +44,7 @@ You can also set the throttling policy on a per-view or per-viewset basis, using the `APIView` class based views. class ExampleView(APIView): - throttle_classes = (UserThrottle,) + throttle_classes = (UserRateThrottle,) def get(self, request, format=None): content = { @@ -55,7 +55,7 @@ using the `APIView` class based views. Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @throttle_classes(UserThrottle) + @throttle_classes(UserRateThrottle) def example_view(request, format=None): content = { 'status': 'request was permitted' @@ -72,22 +72,22 @@ The throttle classes provided by REST framework use Django's cache backend. You ## AnonRateThrottle -The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against. +The `AnonRateThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against. The allowed request rate is determined from one of the following (in order of preference). -* The `rate` property on the class, which may be provided by overriding `AnonThrottle` and setting the property. +* The `rate` property on the class, which may be provided by overriding `AnonRateThrottle` and setting the property. * The `DEFAULT_THROTTLE_RATES['anon']` setting. -`AnonThrottle` is suitable if you want to restrict the rate of requests from unknown sources. +`AnonRateThrottle` is suitable if you want to restrict the rate of requests from unknown sources. ## UserRateThrottle -The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against. +The `UserRateThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticated requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against. The allowed request rate is determined from one of the following (in order of preference). -* The `rate` property on the class, which may be provided by overriding `UserThrottle` and setting the property. +* The `rate` property on the class, which may be provided by overriding `UserRateThrottle` and setting the property. * The `DEFAULT_THROTTLE_RATES['user']` setting. An API may have multiple `UserRateThrottles` in place at the same time. To do so, override `UserRateThrottle` and set a unique "scope" for each class. @@ -113,11 +113,11 @@ For example, multiple user throttle rates could be implemented by using the foll } } -`UserThrottle` is suitable if you want simple global rate restrictions per-user. +`UserRateThrottle` is suitable if you want simple global rate restrictions per-user. ## ScopedRateThrottle -The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address. +The `ScopedRateThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unique user id or IP address. The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope". From 0fa9866848238ed355461a619e5aa9a148403f5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Fri, 26 Jul 2013 09:26:32 +0200 Subject: [PATCH 38/48] Add @willkg for #1007 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index f6b59ed4b..dcd147e36 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -148,6 +148,7 @@ The following people have helped make REST framework great. * Gertjan Oude Lohuis - [gertjanol] * Matthias Jacob - [cyroxx] * Pavel Zinovkin - [pzinovkin] +* Will Kahn-Greene - [willkg] Many thanks to everyone who's contributed to the project. @@ -332,3 +333,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [gertjanol]: https://github.com/gertjanol [cyroxx]: https://github.com/cyroxx [pzinovkin]: https://github.com/pzinovkin +[willkg]: https://github.com/willkg From 103fed966751680d4ac3dc8125b6807e34bb436a Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 26 Jul 2013 10:59:51 -0400 Subject: [PATCH 39/48] Fixed reversed arguments in assertion --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 023f7ccfb..682a99a47 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -690,7 +690,7 @@ class ModelSerializer(Serializer): assert field_name in ret, \ "Noexistant field '%s' specified in `read_only_fields` " \ "on serializer '%s'." % \ - (self.__class__.__name__, field_name) + (field_name, self.__class__.__name__) ret[field_name].read_only = True return ret From 9088c18da85bc56b363f5f1995159cc26e102254 Mon Sep 17 00:00:00 2001 From: Kevin Brown Date: Fri, 26 Jul 2013 11:03:37 -0400 Subject: [PATCH 40/48] Added to the credits --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index dcd147e36..f6e779961 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -149,6 +149,7 @@ The following people have helped make REST framework great. * Matthias Jacob - [cyroxx] * Pavel Zinovkin - [pzinovkin] * Will Kahn-Greene - [willkg] +* Kevin Brown - [kevin-brown] Many thanks to everyone who's contributed to the project. @@ -334,3 +335,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [cyroxx]: https://github.com/cyroxx [pzinovkin]: https://github.com/pzinovkin [willkg]: https://github.com/willkg +[kevin-brown]: https://github.com/kevin-brown From 3a898a11f7683f0f5448ce4d28af212c04befea7 Mon Sep 17 00:00:00 2001 From: Rodrigo Martell Date: Sat, 27 Jul 2013 10:09:56 +1000 Subject: [PATCH 41/48] Fix Tutorial5 serializers.py typo [Issue #N/A] A slight typo at Tutorial5 causing an Exception "name 'models' is not defined". A subtle one that can confuse Python/Django noobs like me. --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 2e013a946..2cf44bf99 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -80,7 +80,7 @@ We can easily re-write our existing serializers to use hyperlinking. highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') class Meta: - model = models.Snippet + model = Snippet fields = ('url', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style') From e98c229e5aa86f0da872583c08f353d974d9cc98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Sun, 28 Jul 2013 18:51:22 +0200 Subject: [PATCH 42/48] Add @coderigo for #1011 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index f6e779961..95cac7176 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -150,6 +150,7 @@ The following people have helped make REST framework great. * Pavel Zinovkin - [pzinovkin] * Will Kahn-Greene - [willkg] * Kevin Brown - [kevin-brown] +* Rodrigo Martell - [coderigo] Many thanks to everyone who's contributed to the project. @@ -334,5 +335,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [gertjanol]: https://github.com/gertjanol [cyroxx]: https://github.com/cyroxx [pzinovkin]: https://github.com/pzinovkin +[coderigo]: https://github.com/coderigo [willkg]: https://github.com/willkg [kevin-brown]: https://github.com/kevin-brown From 5c4bf5309d2e952fb33c27778ff1f1213ebfd5e0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Jul 2013 09:08:35 +0100 Subject: [PATCH 43/48] Docs about migrations and `post_save`. Documentation detailing requirement to migrate prior to running `createsuperuser` if using an authtoken `post_save` signal. Closes #987. Thanks to @yprez. --- docs/api-guide/authentication.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 5d6e0d91d..ee1282b5a 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -184,9 +184,11 @@ The `obtain_auth_token` view will return a JSON response when valid `username` a Note that the default `obtain_auth_token` view explicitly uses JSON requests and responses, rather than using default renderer and parser classes in your settings. If you need a customized version of the `obtain_auth_token` view, you can do so by overriding the `ObtainAuthToken` view class, and using that in your url conf instead. -#### Custom user models +#### Schema migrations -The `rest_framework.authtoken` app includes a south migration that will create the authtoken table. If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created. +The `rest_framework.authtoken` app includes a south migration that will create the authtoken table. + +If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created. You can do so by inserting a `needed_by` attribute in your user migration: @@ -201,6 +203,12 @@ You can do so by inserting a `needed_by` attribute in your user migration: For more details, see the [south documentation on dependencies][south-dependencies]. +Also not that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: + + python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`. + python manage.py migrate + python manage.py createsuperuser + ## SessionAuthentication This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. From 856dc855c952746f566a6a8de263afe951362dfb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 29 Jul 2013 09:21:20 +0100 Subject: [PATCH 44/48] Notes on customizing BrowsableAPIRenderer. Refs #1001 --- docs/api-guide/renderers.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 869bdc16a..b434efe9a 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -217,6 +217,14 @@ Renders data into HTML for the Browsable API. This renderer will determine whic **.charset**: `utf-8` +#### Customizing BrowsableAPIRenderer + +By default the response content will be rendered with the highest priority renderer apart from `BrowseableAPIRenderer`. If you need to customize this behavior, for example to use HTML as the default return format, but use JSON in the browsable API, you can do so by overriding the `get_default_renderer()` method. For example: + + class CustomBrowsableAPIRenderer(BrowsableAPIRenderer): + def get_default_renderer(self, view): + return JSONRenderer() + ## MultiPartRenderer This renderer is used for rendering HTML multipart form data. **It is not suitable as a response renderer**, but is instead used for creating test requests, using REST framework's [test client and test request factory][testing]. From 1a4ff1567ea4231cde9a2f23725550a754f3f54c Mon Sep 17 00:00:00 2001 From: James Rutherford Date: Mon, 29 Jul 2013 10:16:15 +0100 Subject: [PATCH 45/48] Updated authtoken docs to mention south migrations --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ee1282b5a..fd6bfb56a 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -121,7 +121,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y 'rest_framework.authtoken' ) -Make sure to run `manage.py syncdb` after changing your settings. +Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below). You'll also need to create tokens for your users. From 195b1af7ba34b833fc17f5693d7fbd9c8e7cce78 Mon Sep 17 00:00:00 2001 From: James Rutherford Date: Mon, 29 Jul 2013 10:16:51 +0100 Subject: [PATCH 46/48] Minor typo fix --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index fd6bfb56a..b1ab46227 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -203,7 +203,7 @@ You can do so by inserting a `needed_by` attribute in your user migration: For more details, see the [south documentation on dependencies][south-dependencies]. -Also not that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: +Also note that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`. python manage.py migrate From 205c626d6ec472603207e12f8cf9deb3f00bf729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 29 Jul 2013 15:53:26 +0200 Subject: [PATCH 47/48] Add @jimr for #1013 thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 95cac7176..4cc7f34b8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -151,6 +151,7 @@ The following people have helped make REST framework great. * Will Kahn-Greene - [willkg] * Kevin Brown - [kevin-brown] * Rodrigo Martell - [coderigo] +* James Rutherford - [jimr] Many thanks to everyone who's contributed to the project. @@ -338,3 +339,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [coderigo]: https://github.com/coderigo [willkg]: https://github.com/willkg [kevin-brown]: https://github.com/kevin-brown +[jimr]: https://github.com/jimr From 56d19dcc1c71494e83336a799ccf1f500a5af3b8 Mon Sep 17 00:00:00 2001 From: leandersikma Date: Tue, 30 Jul 2013 11:26:17 +0200 Subject: [PATCH 48/48] Wrong name of the login_base html file. Changed rest_framework/base_login.html to rest_framework/login_base.html (which is the correct name the user should extends in their templates). --- docs/topics/browsable-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index 2ae8cadb9..b2c78f3c6 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -90,7 +90,7 @@ The browsable API makes use of the Bootstrap tooltips component. Any element wi ### Login Template -To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/base_login.html`. +To add branding and customize the look-and-feel of the login template, create a template called `login.html` and add it to your project, eg: `templates/rest_framework/login.html`. The template should extend from `rest_framework/login_base.html`. You can add your site name or branding by including the branding block: