aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore18
-rw-r--r--.travis.yml55
-rw-r--r--.tx/config9
-rw-r--r--CONTRIBUTING.md211
-rw-r--r--MANIFEST.in4
-rw-r--r--README.md216
-rw-r--r--docs/CNAME1
-rw-r--r--[-rwxr-xr-x]docs/api-guide/authentication.md234
-rw-r--r--docs/api-guide/content-negotiation.md12
-rw-r--r--docs/api-guide/exceptions.md115
-rw-r--r--docs/api-guide/fields.md539
-rw-r--r--docs/api-guide/filtering.md175
-rw-r--r--docs/api-guide/format-suffixes.md37
-rw-r--r--[-rwxr-xr-x]docs/api-guide/generic-views.md143
-rw-r--r--docs/api-guide/metadata.md103
-rw-r--r--docs/api-guide/pagination.md352
-rw-r--r--docs/api-guide/parsers.md111
-rw-r--r--docs/api-guide/permissions.md111
-rw-r--r--docs/api-guide/relations.md163
-rw-r--r--docs/api-guide/renderers.md230
-rw-r--r--docs/api-guide/requests.md45
-rw-r--r--docs/api-guide/responses.md8
-rw-r--r--docs/api-guide/reverse.md10
-rw-r--r--docs/api-guide/routers.md221
-rw-r--r--docs/api-guide/serializers.md902
-rw-r--r--docs/api-guide/settings.md199
-rw-r--r--docs/api-guide/status-codes.md26
-rw-r--r--docs/api-guide/testing.md43
-rw-r--r--docs/api-guide/throttling.md40
-rw-r--r--docs/api-guide/validators.md225
-rw-r--r--docs/api-guide/versioning.md205
-rw-r--r--docs/api-guide/views.md28
-rw-r--r--docs/api-guide/viewsets.md48
-rw-r--r--docs/img/autocomplete.pngbin58140 -> 0 bytes
-rw-r--r--docs/img/cursor-pagination.pngbin0 -> 12221 bytes
-rw-r--r--docs/img/labels-and-milestones.pngbin0 -> 84026 bytes
-rw-r--r--docs/img/link-header-pagination.pngbin0 -> 35799 bytes
-rw-r--r--docs/img/logo.pngbin0 -> 41532 bytes
-rw-r--r--docs/img/pages-pagination.pngbin0 -> 10229 bytes
-rw-r--r--docs/img/sponsors/0-eventbrite.pngbin0 -> 22429 bytes
-rw-r--r--docs/img/sponsors/1-cyan.pngbin0 -> 6121 bytes
-rw-r--r--docs/img/sponsors/1-divio.pngbin0 -> 4864 bytes
-rw-r--r--docs/img/sponsors/1-kuwaitnet.pngbin0 -> 3674 bytes
-rw-r--r--docs/img/sponsors/1-lulu.pngbin0 -> 18013 bytes
-rw-r--r--docs/img/sponsors/1-potato.pngbin0 -> 12190 bytes
-rw-r--r--docs/img/sponsors/1-purplebit.pngbin0 -> 9161 bytes
-rw-r--r--docs/img/sponsors/1-runscope.pngbin0 -> 10913 bytes
-rw-r--r--docs/img/sponsors/1-simple-energy.pngbin0 -> 54455 bytes
-rw-r--r--docs/img/sponsors/1-vokal_interactive.pngbin0 -> 22814 bytes
-rw-r--r--docs/img/sponsors/1-wiredrive.pngbin0 -> 8082 bytes
-rw-r--r--docs/img/sponsors/2-byte.pngbin0 -> 13690 bytes
-rw-r--r--docs/img/sponsors/2-compile.pngbin0 -> 3108 bytes
-rw-r--r--docs/img/sponsors/2-crate.pngbin0 -> 8257 bytes
-rw-r--r--docs/img/sponsors/2-cryptico.pngbin0 -> 9970 bytes
-rw-r--r--docs/img/sponsors/2-django.pngbin0 -> 5055 bytes
-rw-r--r--docs/img/sponsors/2-heroku.pngbin0 -> 7337 bytes
-rw-r--r--docs/img/sponsors/2-hipflask.pngbin0 -> 6016 bytes
-rw-r--r--docs/img/sponsors/2-hipo.pngbin0 -> 8111 bytes
-rw-r--r--docs/img/sponsors/2-koordinates.pngbin0 -> 1934 bytes
-rw-r--r--docs/img/sponsors/2-laterpay.pngbin0 -> 2003 bytes
-rw-r--r--docs/img/sponsors/2-lightning_kite.pngbin0 -> 6715 bytes
-rw-r--r--docs/img/sponsors/2-mirus_research.pngbin0 -> 12414 bytes
-rw-r--r--docs/img/sponsors/2-nexthub.pngbin0 -> 2562 bytes
-rw-r--r--docs/img/sponsors/2-opbeat.pngbin0 -> 11603 bytes
-rw-r--r--docs/img/sponsors/2-prorenata.pngbin0 -> 4051 bytes
-rw-r--r--docs/img/sponsors/2-pulsecode.pngbin0 -> 2368 bytes
-rw-r--r--docs/img/sponsors/2-rapasso.pngbin0 -> 13667 bytes
-rw-r--r--docs/img/sponsors/2-rheinwerk_verlag.pngbin0 -> 1562 bytes
-rw-r--r--docs/img/sponsors/2-schuberg_philis.pngbin0 -> 21870 bytes
-rw-r--r--docs/img/sponsors/2-security_compass.pngbin0 -> 4107 bytes
-rw-r--r--docs/img/sponsors/2-sga.pngbin0 -> 11112 bytes
-rw-r--r--docs/img/sponsors/2-singing-horse.pngbin0 -> 20831 bytes
-rw-r--r--docs/img/sponsors/2-sirono.pngbin0 -> 4941 bytes
-rw-r--r--docs/img/sponsors/2-vinta.pngbin0 -> 6844 bytes
-rw-r--r--docs/img/sponsors/2-wusawork.pngbin0 -> 12067 bytes
-rw-r--r--docs/img/sponsors/3-aba.pngbin0 -> 18974 bytes
-rw-r--r--docs/img/sponsors/3-aditium.pngbin0 -> 3028 bytes
-rw-r--r--docs/img/sponsors/3-alwaysdata.pngbin0 -> 9349 bytes
-rw-r--r--docs/img/sponsors/3-ax_semantics.pngbin0 -> 11509 bytes
-rw-r--r--docs/img/sponsors/3-beefarm.pngbin0 -> 13066 bytes
-rw-r--r--docs/img/sponsors/3-blimp.pngbin0 -> 6241 bytes
-rw-r--r--docs/img/sponsors/3-brightloop.pngbin0 -> 6864 bytes
-rw-r--r--docs/img/sponsors/3-cantemo.gifbin0 -> 4526 bytes
-rw-r--r--docs/img/sponsors/3-crosswordtracker.pngbin0 -> 6715 bytes
-rw-r--r--docs/img/sponsors/3-fluxility.pngbin0 -> 10064 bytes
-rw-r--r--docs/img/sponsors/3-garfo.pngbin0 -> 3322 bytes
-rw-r--r--docs/img/sponsors/3-gizmag.pngbin0 -> 5370 bytes
-rw-r--r--docs/img/sponsors/3-holvi.pngbin0 -> 7533 bytes
-rw-r--r--docs/img/sponsors/3-imt_computer_services.pngbin0 -> 70397 bytes
-rw-r--r--docs/img/sponsors/3-infinite_code.pngbin0 -> 21786 bytes
-rw-r--r--docs/img/sponsors/3-ipushpull.pngbin0 -> 10089 bytes
-rw-r--r--docs/img/sponsors/3-isl.pngbin0 -> 19203 bytes
-rw-r--r--docs/img/sponsors/3-life_the_game.pngbin0 -> 5485 bytes
-rw-r--r--docs/img/sponsors/3-makespace.pngbin0 -> 8420 bytes
-rw-r--r--docs/img/sponsors/3-nephila.pngbin0 -> 8415 bytes
-rw-r--r--docs/img/sponsors/3-openeye.pngbin0 -> 14155 bytes
-rw-r--r--docs/img/sponsors/3-pathwright.pngbin0 -> 13036 bytes
-rw-r--r--docs/img/sponsors/3-phurba.pngbin0 -> 3064 bytes
-rw-r--r--docs/img/sponsors/3-pkgfarm.pngbin0 -> 2275 bytes
-rw-r--r--docs/img/sponsors/3-providenz.pngbin0 -> 12580 bytes
-rw-r--r--docs/img/sponsors/3-safari.pngbin0 -> 4419 bytes
-rw-r--r--docs/img/sponsors/3-shippo.pngbin0 -> 7345 bytes
-rw-r--r--docs/img/sponsors/3-teonite.pngbin0 -> 7882 bytes
-rw-r--r--docs/img/sponsors/3-thermondo-gmbh.pngbin0 -> 20046 bytes
-rw-r--r--docs/img/sponsors/3-tivix.pngbin0 -> 3552 bytes
-rw-r--r--docs/img/sponsors/3-trackmaven.pngbin0 -> 5331 bytes
-rw-r--r--docs/img/sponsors/3-transcode.pngbin0 -> 8615 bytes
-rw-r--r--docs/img/sponsors/3-triggered_messaging.pngbin0 -> 10509 bytes
-rw-r--r--docs/img/sponsors/3-vzzual.pngbin0 -> 12008 bytes
-rw-r--r--docs/img/sponsors/3-wildfish.pngbin0 -> 4137 bytes
-rw-r--r--docs/img/travis-status.pngbin0 -> 10023 bytes
-rw-r--r--docs/index.md193
-rw-r--r--docs/requirements.txt1
-rw-r--r--docs/template.html212
-rw-r--r--docs/topics/2.2-announcement.md18
-rw-r--r--docs/topics/2.3-announcement.md34
-rw-r--r--docs/topics/2.4-announcement.md172
-rw-r--r--docs/topics/3.0-announcement.md965
-rw-r--r--docs/topics/3.1-announcement.md209
-rw-r--r--docs/topics/ajax-csrf-cors.md8
-rw-r--r--docs/topics/browsable-api.md45
-rw-r--r--docs/topics/contributing.md155
-rw-r--r--docs/topics/credits.md340
-rw-r--r--docs/topics/documenting-your-api.md18
-rw-r--r--docs/topics/internationalization.md113
-rw-r--r--docs/topics/kickstarter-announcement.md163
-rw-r--r--docs/topics/project-management.md200
-rw-r--r--docs/topics/release-notes.md712
-rw-r--r--docs/topics/rest-framework-2-announcement.md8
-rw-r--r--docs/topics/rest-hypermedia-hateoas.md12
-rw-r--r--docs/topics/third-party-resources.md328
-rw-r--r--docs/topics/writable-nested-serializers.md47
-rw-r--r--docs/tutorial/1-serialization.md219
-rw-r--r--docs/tutorial/2-requests-and-responses.md98
-rw-r--r--docs/tutorial/3-class-based-views.md24
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md83
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md47
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md40
-rw-r--r--docs/tutorial/quickstart.md125
-rw-r--r--docs_theme/404.html216
-rw-r--r--docs_theme/base.html197
-rwxr-xr-xdocs_theme/css/bootstrap-responsive.css (renamed from docs/css/bootstrap-responsive.css)0
-rwxr-xr-xdocs_theme/css/bootstrap.css (renamed from docs/css/bootstrap.css)0
-rw-r--r--docs_theme/css/default.css (renamed from docs/css/default.css)112
-rw-r--r--docs_theme/css/prettify.css (renamed from docs/css/prettify.css)0
-rw-r--r--docs_theme/img/favicon.ico (renamed from docs/img/favicon.ico)bin2238 -> 2238 bytes
-rw-r--r--docs_theme/img/glyphicons-halflings-white.png (renamed from docs/img/glyphicons-halflings-white.png)bin8777 -> 8777 bytes
-rw-r--r--docs_theme/img/glyphicons-halflings.png (renamed from docs/img/glyphicons-halflings.png)bin12799 -> 12799 bytes
-rw-r--r--docs_theme/img/grid.png (renamed from docs/img/grid.png)bin1458 -> 1458 bytes
-rwxr-xr-xdocs_theme/js/bootstrap-2.1.1-min.js (renamed from docs/js/bootstrap-2.1.1-min.js)0
-rw-r--r--docs_theme/js/jquery-1.8.1-min.js (renamed from docs/js/jquery-1.8.1-min.js)0
-rw-r--r--docs_theme/js/prettify-1.0.js (renamed from docs/js/prettify-1.0.js)0
-rw-r--r--docs_theme/js/theme.js5
-rw-r--r--docs_theme/nav.html47
-rwxr-xr-xmkdocs.py185
-rw-r--r--mkdocs.yml56
-rw-r--r--optionals.txt7
-rw-r--r--requirements.txt14
-rw-r--r--requirements/requirements-codestyle.txt3
-rw-r--r--requirements/requirements-documentation.txt2
-rw-r--r--requirements/requirements-optionals.txt4
-rw-r--r--requirements/requirements-packaging.txt8
-rw-r--r--requirements/requirements-testing.txt3
-rw-r--r--rest_framework/__init__.py18
-rw-r--r--rest_framework/authentication.py205
-rw-r--r--rest_framework/authtoken/migrations/0001_initial.py91
-rw-r--r--rest_framework/authtoken/models.py21
-rw-r--r--rest_framework/authtoken/serializers.py18
-rw-r--r--rest_framework/authtoken/south_migrations/0001_initial.py60
-rw-r--r--rest_framework/authtoken/south_migrations/__init__.py (renamed from rest_framework/runtests/__init__.py)0
-rw-r--r--rest_framework/authtoken/views.py13
-rw-r--r--rest_framework/compat.py615
-rw-r--r--rest_framework/decorators.py23
-rw-r--r--rest_framework/exceptions.py126
-rw-r--r--rest_framework/fields.py1742
-rw-r--r--rest_framework/filters.py97
-rw-r--r--rest_framework/generics.py410
-rw-r--r--rest_framework/locale/ar/LC_MESSAGES/django.mobin0 -> 4875 bytes
-rw-r--r--rest_framework/locale/ar/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/cs/LC_MESSAGES/django.mobin0 -> 8848 bytes
-rw-r--r--rest_framework/locale/cs/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/da/LC_MESSAGES/django.mobin0 -> 8452 bytes
-rw-r--r--rest_framework/locale/da/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/de/LC_MESSAGES/django.mobin0 -> 6575 bytes
-rw-r--r--rest_framework/locale/de/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/en/LC_MESSAGES/django.mobin0 -> 8553 bytes
-rw-r--r--rest_framework/locale/en/LC_MESSAGES/django.po324
-rw-r--r--rest_framework/locale/en_US/LC_MESSAGES/django.mobin0 -> 378 bytes
-rw-r--r--rest_framework/locale/en_US/LC_MESSAGES/django.po326
-rw-r--r--rest_framework/locale/es/LC_MESSAGES/django.mobin0 -> 8906 bytes
-rw-r--r--rest_framework/locale/es/LC_MESSAGES/django.po327
-rw-r--r--rest_framework/locale/et/LC_MESSAGES/django.mobin0 -> 2018 bytes
-rw-r--r--rest_framework/locale/et/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/fr/LC_MESSAGES/django.mobin0 -> 6975 bytes
-rw-r--r--rest_framework/locale/fr/LC_MESSAGES/django.po326
-rw-r--r--rest_framework/locale/hu/LC_MESSAGES/django.mobin0 -> 9215 bytes
-rw-r--r--rest_framework/locale/hu/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/id/LC_MESSAGES/django.mobin0 -> 497 bytes
-rw-r--r--rest_framework/locale/id/LC_MESSAGES/django.po324
-rw-r--r--rest_framework/locale/it/LC_MESSAGES/django.mobin0 -> 5334 bytes
-rw-r--r--rest_framework/locale/it/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/ko_KR/LC_MESSAGES/django.mobin0 -> 8555 bytes
-rw-r--r--rest_framework/locale/ko_KR/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/mk/LC_MESSAGES/django.mobin0 -> 10731 bytes
-rw-r--r--rest_framework/locale/mk/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/nl/LC_MESSAGES/django.mobin0 -> 499 bytes
-rw-r--r--rest_framework/locale/nl/LC_MESSAGES/django.po324
-rw-r--r--rest_framework/locale/pl/LC_MESSAGES/django.mobin0 -> 8981 bytes
-rw-r--r--rest_framework/locale/pl/LC_MESSAGES/django.po326
-rw-r--r--rest_framework/locale/pt_BR/LC_MESSAGES/django.mobin0 -> 8729 bytes
-rw-r--r--rest_framework/locale/pt_BR/LC_MESSAGES/django.po326
-rw-r--r--rest_framework/locale/ru/LC_MESSAGES/django.mobin0 -> 9778 bytes
-rw-r--r--rest_framework/locale/ru/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/sk/LC_MESSAGES/django.mobin0 -> 2748 bytes
-rw-r--r--rest_framework/locale/sk/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/sv/LC_MESSAGES/django.mobin0 -> 8647 bytes
-rw-r--r--rest_framework/locale/sv/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/locale/tr/LC_MESSAGES/django.mobin0 -> 7946 bytes
-rw-r--r--rest_framework/locale/tr/LC_MESSAGES/django.po328
-rw-r--r--rest_framework/locale/uk/LC_MESSAGES/django.mobin0 -> 577 bytes
-rw-r--r--rest_framework/locale/uk/LC_MESSAGES/django.po324
-rw-r--r--rest_framework/locale/zh_CN/LC_MESSAGES/django.mobin0 -> 8383 bytes
-rw-r--r--rest_framework/locale/zh_CN/LC_MESSAGES/django.po325
-rw-r--r--rest_framework/metadata.py138
-rw-r--r--rest_framework/mixins.py152
-rw-r--r--rest_framework/negotiation.py10
-rw-r--r--rest_framework/pagination.py747
-rw-r--r--rest_framework/parsers.py149
-rw-r--r--rest_framework/permissions.py108
-rw-r--r--rest_framework/relations.py809
-rw-r--r--rest_framework/renderers.py668
-rw-r--r--rest_framework/request.py172
-rw-r--r--rest_framework/response.py22
-rw-r--r--rest_framework/reverse.py17
-rw-r--r--rest_framework/routers.py126
-rwxr-xr-xrest_framework/runtests/runcoverage.py78
-rwxr-xr-xrest_framework/runtests/runtests.py48
-rw-r--r--rest_framework/runtests/settings.py151
-rw-r--r--rest_framework/runtests/urls.py7
-rw-r--r--rest_framework/serializers.py1896
-rw-r--r--rest_framework/settings.py115
-rw-r--r--rest_framework/six.py389
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap-tweaks.css156
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap.min.css844
-rw-r--r--rest_framework/static/rest_framework/css/default.css43
-rw-r--r--rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eotbin0 -> 20335 bytes
-rw-r--r--rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg229
-rw-r--r--rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttfbin0 -> 41280 bytes
-rw-r--r--rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woffbin0 -> 23320 bytes
-rw-r--r--rest_framework/static/rest_framework/js/bootstrap.min.js13
-rw-r--r--rest_framework/static/rest_framework/js/default.js48
-rw-r--r--rest_framework/status.py21
-rw-r--r--rest_framework/templates/rest_framework/api_form.html8
-rw-r--r--rest_framework/templates/rest_framework/base.html431
-rw-r--r--rest_framework/templates/rest_framework/form.html13
-rw-r--r--rest_framework/templates/rest_framework/horizontal/checkbox.html16
-rw-r--r--rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html30
-rw-r--r--rest_framework/templates/rest_framework/horizontal/fieldset.html13
-rw-r--r--rest_framework/templates/rest_framework/horizontal/form.html15
-rw-r--r--rest_framework/templates/rest_framework/horizontal/input.html14
-rw-r--r--rest_framework/templates/rest_framework/horizontal/list_fieldset.html16
-rw-r--r--rest_framework/templates/rest_framework/horizontal/radio.html30
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select.html21
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select_multiple.html23
-rw-r--r--rest_framework/templates/rest_framework/horizontal/textarea.html14
-rw-r--r--rest_framework/templates/rest_framework/inline/checkbox.html8
-rw-r--r--rest_framework/templates/rest_framework/inline/checkbox_multiple.html13
-rw-r--r--rest_framework/templates/rest_framework/inline/fieldset.html6
-rw-r--r--rest_framework/templates/rest_framework/inline/form.html11
-rw-r--r--rest_framework/templates/rest_framework/inline/input.html6
-rw-r--r--rest_framework/templates/rest_framework/inline/list_fieldset.html1
-rw-r--r--rest_framework/templates/rest_framework/inline/radio.html13
-rw-r--r--rest_framework/templates/rest_framework/inline/select.html13
-rw-r--r--rest_framework/templates/rest_framework/inline/select_multiple.html15
-rw-r--r--rest_framework/templates/rest_framework/inline/textarea.html6
-rw-r--r--rest_framework/templates/rest_framework/login_base.html59
-rw-r--r--rest_framework/templates/rest_framework/pagination/numbers.html27
-rw-r--r--rest_framework/templates/rest_framework/pagination/previous_and_next.html12
-rw-r--r--rest_framework/templates/rest_framework/raw_data_form.html12
-rw-r--r--rest_framework/templates/rest_framework/vertical/checkbox.html14
-rw-r--r--rest_framework/templates/rest_framework/vertical/checkbox_multiple.html30
-rw-r--r--rest_framework/templates/rest_framework/vertical/fieldset.html9
-rw-r--r--rest_framework/templates/rest_framework/vertical/form.html11
-rw-r--r--rest_framework/templates/rest_framework/vertical/input.html12
-rw-r--r--rest_framework/templates/rest_framework/vertical/list_fieldset.html4
-rw-r--r--rest_framework/templates/rest_framework/vertical/radio.html30
-rw-r--r--rest_framework/templates/rest_framework/vertical/select.html19
-rw-r--r--rest_framework/templates/rest_framework/vertical/select_multiple.html21
-rw-r--r--rest_framework/templates/rest_framework/vertical/textarea.html12
-rw-r--r--rest_framework/templatetags/rest_framework.py168
-rw-r--r--rest_framework/test.py84
-rw-r--r--rest_framework/tests/extras/bad_import.py1
-rw-r--r--rest_framework/tests/models.py169
-rw-r--r--rest_framework/tests/test_authentication.py635
-rw-r--r--rest_framework/tests/test_breadcrumbs.py73
-rw-r--r--rest_framework/tests/test_fields.py898
-rw-r--r--rest_framework/tests/test_files.py51
-rw-r--r--rest_framework/tests/test_filters.py474
-rw-r--r--rest_framework/tests/test_hyperlinkedserializers.py333
-rw-r--r--rest_framework/tests/test_pagination.py385
-rw-r--r--rest_framework/tests/test_parsers.py115
-rw-r--r--rest_framework/tests/test_permissions.py188
-rw-r--r--rest_framework/tests/test_relations.py100
-rw-r--r--rest_framework/tests/test_relations_nested.py107
-rw-r--r--rest_framework/tests/test_routers.py216
-rw-r--r--rest_framework/tests/test_serializer.py1645
-rw-r--r--rest_framework/tests/test_serializer_bulk_update.py278
-rw-r--r--rest_framework/tests/test_serializer_nested.py246
-rw-r--r--rest_framework/tests/test_settings.py22
-rw-r--r--rest_framework/tests/test_testing.py115
-rw-r--r--rest_framework/tests/test_validation.py85
-rw-r--r--rest_framework/tests/tests.py16
-rw-r--r--rest_framework/throttling.py45
-rw-r--r--rest_framework/urlpatterns.py4
-rw-r--r--rest_framework/urls.py20
-rw-r--r--rest_framework/utils/breadcrumbs.py6
-rw-r--r--rest_framework/utils/encoders.py124
-rw-r--r--rest_framework/utils/field_mapping.py249
-rw-r--r--rest_framework/utils/formatting.py43
-rw-r--r--rest_framework/utils/html.py88
-rw-r--r--rest_framework/utils/humanize_datetime.py47
-rw-r--r--rest_framework/utils/mediatypes.py9
-rw-r--r--rest_framework/utils/model_meta.py169
-rw-r--r--rest_framework/utils/representation.py99
-rw-r--r--rest_framework/utils/serializer_helpers.py120
-rw-r--r--rest_framework/utils/urls.py25
-rw-r--r--rest_framework/validators.py261
-rw-r--r--rest_framework/versioning.py177
-rw-r--r--rest_framework/views.py233
-rw-r--r--rest_framework/viewsets.py27
-rwxr-xr-xruntests.py91
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py16
-rw-r--r--tests/__init__.py (renamed from rest_framework/tests/__init__.py)0
-rw-r--r--tests/browsable_api/__init__.py (renamed from rest_framework/tests/extras/__init__.py)0
-rw-r--r--tests/browsable_api/auth_urls.py11
-rw-r--r--tests/browsable_api/no_auth_urls.py9
-rw-r--r--tests/browsable_api/test_browsable_api.py65
-rw-r--r--tests/browsable_api/views.py15
-rw-r--r--tests/conftest.py66
-rw-r--r--tests/description.py (renamed from rest_framework/tests/description.py)0
-rw-r--r--tests/models.py70
-rw-r--r--tests/test_authentication.py288
-rw-r--r--tests/test_bound_fields.py69
-rw-r--r--tests/test_decorators.py (renamed from rest_framework/tests/test_decorators.py)0
-rw-r--r--tests/test_description.py (renamed from rest_framework/tests/test_description.py)38
-rw-r--r--tests/test_fields.py1212
-rw-r--r--tests/test_filters.py823
-rw-r--r--tests/test_generics.py (renamed from rest_framework/tests/test_generics.py)294
-rw-r--r--tests/test_htmlrenderer.py (renamed from rest_framework/tests/test_htmlrenderer.py)27
-rw-r--r--tests/test_metadata.py209
-rw-r--r--tests/test_middleware.py37
-rw-r--r--tests/test_model_serializer.py641
-rw-r--r--tests/test_multitable_inheritance.py (renamed from rest_framework/tests/test_multitable_inheritance.py)8
-rw-r--r--tests/test_negotiation.py (renamed from rest_framework/tests/test_negotiation.py)0
-rw-r--r--tests/test_pagination.py671
-rw-r--r--tests/test_parsers.py103
-rw-r--r--tests/test_permissions.py312
-rw-r--r--tests/test_relations.py169
-rw-r--r--tests/test_relations_generic.py (renamed from rest_framework/tests/test_genericrelations.py)42
-rw-r--r--tests/test_relations_hyperlink.py (renamed from rest_framework/tests/test_relations_hyperlink.py)148
-rw-r--r--tests/test_relations_pk.py (renamed from rest_framework/tests/test_relations_pk.py)218
-rw-r--r--tests/test_relations_slug.py (renamed from rest_framework/tests/test_relations_slug.py)42
-rw-r--r--tests/test_renderers.py (renamed from rest_framework/tests/test_renderers.py)454
-rw-r--r--tests/test_request.py (renamed from rest_framework/tests/test_request.py)168
-rw-r--r--tests/test_response.py (renamed from rest_framework/tests/test_response.py)54
-rw-r--r--tests/test_reverse.py (renamed from rest_framework/tests/test_reverse.py)7
-rw-r--r--tests/test_routers.py348
-rw-r--r--tests/test_serializer.py297
-rw-r--r--tests/test_serializer_bulk_update.py123
-rw-r--r--tests/test_serializer_lists.py290
-rw-r--r--tests/test_serializer_nested.py40
-rw-r--r--tests/test_settings.py17
-rw-r--r--tests/test_status.py33
-rw-r--r--tests/test_templatetags.py75
-rw-r--r--tests/test_testing.py234
-rw-r--r--tests/test_throttling.py (renamed from rest_framework/tests/test_throttling.py)155
-rw-r--r--tests/test_urlpatterns.py (renamed from rest_framework/tests/test_urlpatterns.py)2
-rw-r--r--tests/test_utils.py166
-rw-r--r--tests/test_validation.py183
-rw-r--r--tests/test_validators.py347
-rw-r--r--tests/test_versioning.py264
-rw-r--r--tests/test_views.py (renamed from rest_framework/tests/test_views.py)57
-rw-r--r--tests/test_viewsets.py35
-rw-r--r--tests/test_write_only_fields.py31
-rw-r--r--tests/urls.py6
-rw-r--r--tests/utils.py77
-rw-r--r--tox.ini127
388 files changed, 30880 insertions, 15926 deletions
diff --git a/.gitignore b/.gitignore
index ae73f837..3d5f1043 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,18 +3,14 @@
*~
.*
-html/
-htmlcov/
-coverage/
-build/
-dist/
-*.egg-info/
+/site/
+/htmlcov/
+/coverage/
+/build/
+/dist/
+/*.egg-info/
+/env/
MANIFEST
-bin/
-include/
-lib/
-local/
-
!.gitignore
!.travis.yml
diff --git a/.travis.yml b/.travis.yml
index 6a453241..3eb89dc4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,38 +1,33 @@
language: python
-python:
- - "2.6"
- - "2.7"
- - "3.2"
- - "3.3"
+sudo: false
env:
- - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
- - DJANGO="django==1.5.1 --use-mirrors"
- - DJANGO="django==1.4.5 --use-mirrors"
- - DJANGO="django==1.3.7 --use-mirrors"
+ - TOX_ENV=py27-flake8
+ - TOX_ENV=py27-docs
+ - TOX_ENV=py34-django17
+ - TOX_ENV=py33-django17
+ - TOX_ENV=py32-django17
+ - TOX_ENV=py27-django17
+ - TOX_ENV=py34-django16
+ - TOX_ENV=py33-django16
+ - TOX_ENV=py32-django16
+ - TOX_ENV=py27-django16
+ - TOX_ENV=py26-django16
+ - TOX_ENV=py34-django15
+ - TOX_ENV=py33-django15
+ - TOX_ENV=py32-django15
+ - TOX_ENV=py27-django15
+ - TOX_ENV=py26-django15
+ - TOX_ENV=py27-django14
+ - TOX_ENV=py26-django14
+ - TOX_ENV=py34-django18beta
+ - TOX_ENV=py33-django18beta
+ - TOX_ENV=py32-django18beta
+ - TOX_ENV=py27-django18beta
install:
- - pip install $DJANGO
- - pip install defusedxml==0.3
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
- - export PYTHONPATH=.
+ - pip install tox
script:
- - python rest_framework/runtests/runtests.py
-
-matrix:
- exclude:
- - python: "3.2"
- env: DJANGO="django==1.4.5 --use-mirrors"
- - python: "3.2"
- env: DJANGO="django==1.3.7 --use-mirrors"
- - python: "3.3"
- env: DJANGO="django==1.4.5 --use-mirrors"
- - python: "3.3"
- env: DJANGO="django==1.3.7 --use-mirrors"
-
+ - tox -e $TOX_ENV
diff --git a/.tx/config b/.tx/config
new file mode 100644
index 00000000..271fa1e3
--- /dev/null
+++ b/.tx/config
@@ -0,0 +1,9 @@
+[main]
+host = https://www.transifex.com
+
+[django-rest-framework.djangopo]
+file_filter = rest_framework/locale/<lang>/LC_MESSAGES/django.po
+source_file = rest_framework/locale/en_US/LC_MESSAGES/django.po
+source_lang = en_US
+type = PO
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..c9626ebf
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,211 @@
+# Contributing to REST framework
+
+> The world can only really be changed one piece at a time. The art is picking that piece.
+>
+> &mdash; [Tim Berners-Lee][cite]
+
+There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
+
+## Community
+
+The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
+
+If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
+
+Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
+
+When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
+
+## Code of conduct
+
+Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
+
+Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
+
+The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
+
+# Issues
+
+It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
+
+Some tips on good issue reporting:
+
+* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
+* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
+* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
+* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
+* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
+
+## Triaging issues
+
+Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
+
+* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
+* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
+* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
+* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
+* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
+
+# Development
+
+To start developing on Django REST framework, clone the repo:
+
+ git clone git@github.com:tomchristie/django-rest-framework.git
+
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
+
+## Testing
+
+To run the tests, clone the repository, and then:
+
+ # Setup the virtual environment
+ virtualenv env
+ source env/bin/activate
+ pip install -r requirements.txt
+
+ # Run the tests
+ ./runtests.py
+
+### Test options
+
+Run using a more concise output style.
+
+ ./runtests.py -q
+
+Run the tests using a more concise output style, no coverage, no flake8.
+
+ ./runtests.py --fast
+
+Don't run the flake8 code linting.
+
+ ./runtests.py --nolint
+
+Only run the flake8 code linting, don't run the tests.
+
+ ./runtests.py --lintonly
+
+Run the tests for a given test case.
+
+ ./runtests.py MyTestCase
+
+Run the tests for a given test method.
+
+ ./runtests.py MyTestCase.test_this_method
+
+Shorter form to run the tests for a given test method.
+
+ ./runtests.py test_this_method
+
+Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
+
+### Running against multiple environments
+
+You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
+
+ tox
+
+## Pull requests
+
+It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
+
+It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests.
+
+It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
+
+GitHub's documentation for working on pull requests is [available here][pull-requests].
+
+Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
+
+Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
+
+![Travis status][travis-status]
+
+*Above: Travis build notifications*
+
+## Managing compatibility issues
+
+Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
+
+# Documentation
+
+The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
+
+There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
+
+## Building the documentation
+
+To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command.
+
+ mkdocs build
+
+This will build the documentation into the `site` directory.
+
+You can build the documentation and open a preview in a browser window by using the `serve` command.
+
+ mkdocs serve
+
+## Language style
+
+Documentation should be in American English. The tone of the documentation is very important - try to stick to a simple, plain, objective and well-balanced style where possible.
+
+Some other tips:
+
+* Keep paragraphs reasonably short.
+* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
+
+## Markdown style
+
+There are a couple of conventions you should follow when working on the documentation.
+
+##### 1. Headers
+
+Headers should use the hash style. For example:
+
+ ### Some important topic
+
+The underline style should not be used. **Don't do this:**
+
+ Some important topic
+ ====================
+
+##### 2. Links
+
+Links should always use the reference style, with the referenced hyperlinks kept at the end of the document.
+
+ Here is a link to [some other thing][other-thing].
+
+ More text...
+
+ [other-thing]: http://example.com/other/thing
+
+This style helps keep the documentation source consistent and readable.
+
+If you are hyperlinking to another REST framework document, you should use a relative link, and link to the `.md` suffix. For example:
+
+ [authentication]: ../api-guide/authentication.md
+
+Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
+
+##### 3. Notes
+
+If you want to draw attention to a note or warning, use a pair of enclosing lines, like so:
+
+ ---
+
+ **Note:** A useful documentation note.
+
+ ---
+
+
+[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
+[code-of-conduct]: https://www.djangoproject.com/conduct/
+[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
+[so-filter]: http://stackexchange.com/filters/66475/rest-framework
+[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
+[pep-8]: http://www.python.org/dev/peps/pep-0008/
+[travis-status]: ../img/travis-status.png
+[pull-requests]: https://help.github.com/articles/using-pull-requests
+[tox]: http://tox.readthedocs.org/en/latest/
+[markdown]: http://daringfireball.net/projects/markdown/basics
+[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
+[mou]: http://mouapp.com/
diff --git a/MANIFEST.in b/MANIFEST.in
index 15c4d0b0..d202c86e 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,4 @@
-recursive-include rest_framework/static *.js *.css *.png
+recursive-include rest_framework/static *.js *.css *.png *.eot *.svg *.ttf *.woff
recursive-include rest_framework/templates *.html
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
diff --git a/README.md b/README.md
index 62883e32..5044ee0f 100644
--- a/README.md
+++ b/README.md
@@ -1,33 +1,42 @@
-# Django REST framework
-
-**Awesome web-browseable Web APIs.**
+# [Django REST framework][docs]
[![build-status-image]][travis]
+[![pypi-version]][pypi]
+
+**Awesome web-browsable Web APIs.**
+
+Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
+
+---
+
+**Note**: We have now released Django REST framework 3.1. For older codebases you may want to refer to the version 2.4.4 [source code](https://github.com/tomchristie/django-rest-framework/tree/version-2.4.x), and [documentation](http://tomchristie.github.io/rest-framework-2-docs/).
+
+For more details see the [3.1 release notes][3.1-announcement]
-**Note**: Full documentation for the project is available at [http://django-rest-framework.org][docs].
+---
# Overview
-Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
+Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
-* The [Web browseable API][sandbox] is a huge useability win for your developers.
-* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
+* The [Web browsable API][sandbox] is a huge usability win for your developers.
+* [Authentication policies][authentication] including optional packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
-* [Extensive documentation][index], and [great community support][group].
+* [Extensive documentation][docs], and [great community support][group].
There is a live example API for testing purposes, [available here][sandbox].
-**Below**: *Screenshot from the browseable API*
+**Below**: *Screenshot from the browsable API*
![Screenshot][image]
# Requirements
-* Python (2.6.5+, 2.7, 3.2, 3.3)
-* Django (1.3, 1.4, 1.5, 1.6)
+* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
+* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta)
# Installation
@@ -39,64 +48,103 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
- 'rest_framework',
+ 'rest_framework',
)
# Example
Let's take a look at a quick example of using REST framework to build a simple model-backed API for accessing users and groups.
-Here's our project's root `urls.py` module:
+Startup up a new project like so...
- from django.conf.urls.defaults import url, patterns, include
- from django.contrib.auth.models import User, Group
- from rest_framework import viewsets, routers
+ pip install django
+ pip install djangorestframework
+ django-admin.py startproject example .
+ ./manage.py syncdb
+
+Now edit the `example/urls.py` module in your project:
+
+```python
+from django.conf.urls import url, include
+from django.contrib.auth.models import User
+from rest_framework import serializers, viewsets, routers
- # ViewSets define the view behavior.
- class UserViewSet(viewsets.ModelViewSet):
+# Serializers define the API representation.
+class UserSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
model = User
+ fields = ('url', 'username', 'email', 'is_staff')
- class GroupViewSet(viewsets.ModelViewSet):
- model = Group
-
- # Routers provide an easy way of automatically determining the URL conf
- router = routers.DefaultRouter()
- router.register(r'users', UserViewSet)
- router.register(r'groups', GroupViewSet)
+# ViewSets define the view behavior.
+class UserViewSet(viewsets.ModelViewSet):
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
- # Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
- url(r'^', include(router.urls)),
- url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+# Routers provide a way of automatically determining the URL conf.
+router = routers.DefaultRouter()
+router.register(r'users', UserViewSet)
+
+
+# Wire up our API using automatic URL routing.
+# Additionally, we include login URLs for the browsable API.
+urlpatterns = [
+ url(r'^', include(router.urls)),
+ url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
+]
+```
We'd also like to configure a couple of settings for our API.
Add the following to your `settings.py` module:
- REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
-
- # Use Django's standard `django.contrib.auth` permissions,
- # or allow read-only access for unauthenticated users.
- 'DEFAULT_PERMISSION_CLASSES': [
- 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
- ]
- }
-
-Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
+```python
+INSTALLED_APPS = (
+ ... # Make sure to include the default installed apps here.
+ 'rest_framework',
+)
+
+REST_FRAMEWORK = {
+ # Use Django's standard `django.contrib.auth` permissions,
+ # or allow read-only access for unauthenticated users.
+ 'DEFAULT_PERMISSION_CLASSES': [
+ 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
+ ]
+}
+```
That's it, we're done!
+ ./manage.py runserver
+
+You can now open the API in your browser at `http://127.0.0.1:8000/`, and view your new 'users' API. If you use the `Login` control in the top right corner you'll also be able to add, create and delete users from the system.
+
+You can also interact with the API using command line tools such as [`curl`](http://curl.haxx.se/). For example, to list the users endpoint:
+
+ $ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ [
+ {
+ "url": "http://127.0.0.1:8000/users/1/",
+ "username": "admin",
+ "email": "admin@example.com",
+ "is_staff": true,
+ }
+ ]
+
+Or to create a new user:
+
+ $ curl -X POST -d username=new -d email=new@example.com -d is_staff=false -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ {
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "new",
+ "email": "new@example.com",
+ "is_staff": false,
+ }
+
# Documentation & Support
-Full documentation for the project is available at [http://django-rest-framework.org][docs].
+Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
For questions and support, use the [REST framework discussion group][group], or `#restframework` on freenode IRC.
@@ -110,62 +158,50 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
# License
-Copyright (c) 2011-2013, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
-Redistribution and use in source and binary forms, with or without
+Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this
+Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this
-list of conditions and the following disclaimer in the documentation and/or
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
+
+[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
+[pypi-version]: https://pypip.in/version/djangorestframework/badge.svg
+[pypi]: https://pypi.python.org/pypi/djangorestframework
[twitter]: https://twitter.com/_tomchristie
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
-[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[sandbox]: http://restframework.herokuapp.com/
-[index]: http://django-rest-framework.org/
-[oauth1-section]: http://django-rest-framework.org/api-guide/authentication.html#oauthauthentication
-[oauth2-section]: http://django-rest-framework.org/api-guide/authentication.html#oauth2authentication
-[serializer-section]: http://django-rest-framework.org/api-guide/serializers.html#serializers
-[modelserializer-section]: http://django-rest-framework.org/api-guide/serializers.html#modelserializer
-[functionview-section]: http://django-rest-framework.org/api-guide/views.html#function-based-views
-[generic-views]: http://django-rest-framework.org/api-guide/generic-views.html
-[viewsets]: http://django-rest-framework.org/api-guide/viewsets.html
-[routers]: http://django-rest-framework.org/api-guide/routers.html
-[serializers]: http://django-rest-framework.org/api-guide/serializers.html
-[authentication]: http://django-rest-framework.org/api-guide/authentication.html
-
-[rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html
-[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
-[image]: http://django-rest-framework.org/img/quickstart.png
-
-[tox]: http://testrun.org/tox/latest/
-
-[tehjones]: https://twitter.com/tehjones/status/294986071979196416
-[wlonk]: https://twitter.com/wlonk/status/261689665952833536
-[laserllama]: https://twitter.com/laserllama/status/328688333750407168
-
-[docs]: http://django-rest-framework.org/
-[urlobject]: https://github.com/zacharyvoase/urlobject
-[markdown]: http://pypi.python.org/pypi/Markdown/
-[pyyaml]: http://pypi.python.org/pypi/PyYAML
-[defusedxml]: https://pypi.python.org/pypi/defusedxml
-[django-filter]: http://pypi.python.org/pypi/django-filter
+[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#django-oauth-toolkit
+[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#serializers
+[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#modelserializer
+[functionview-section]: http://www.django-rest-framework.org/api-guide/views/#function-based-views
+[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views/
+[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets/
+[routers]: http://www.django-rest-framework.org/api-guide/routers/
+[serializers]: http://www.django-rest-framework.org/api-guide/serializers/
+[authentication]: http://www.django-rest-framework.org/api-guide/authentication/
+[image]: http://www.django-rest-framework.org/img/quickstart.png
+
+[docs]: http://www.django-rest-framework.org/
[security-mail]: mailto:rest-framework-security@googlegroups.com
+[3.1-announcement]: http://www.django-rest-framework.org/topics/3.1-announcement/
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 00000000..4e332899
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+www.django-rest-framework.org
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index ee1282b5..fe1be7bf 100755..100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -1,4 +1,4 @@
-<a class="github" href="authentication.py"></a>
+source: authentication.py
# Authentication
@@ -34,7 +34,7 @@ The value of `request.user` and `request.auth` for unauthenticated requests can
## Setting the authentication scheme
-The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION` setting. For example.
+The default authentication schemes may be set globally, using the `DEFAULT_AUTHENTICATION_CLASSES` setting. For example.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
@@ -46,6 +46,11 @@ The default authentication schemes may be set globally, using the `DEFAULT_AUTHE
You can also set the authentication scheme on a per-view or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.authentication import SessionAuthentication, BasicAuthentication
+ from rest_framework.permissions import IsAuthenticated
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,)
@@ -88,7 +93,7 @@ Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the author
If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`.
- # this can go in either server config, virtual host, directory or .htaccess
+ # this can go in either server config, virtual host, directory or .htaccess
WSGIPassAuthorization On
---
@@ -112,16 +117,21 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
## TokenAuthentication
-This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
+This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
-To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
+To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
INSTALLED_APPS = (
...
'rest_framework.authtoken'
)
-
-Make sure to run `manage.py syncdb` after changing your settings.
+
+---
+
+**Note:** Make sure to run `manage.py syncdb` after changing your settings. The `rest_framework.authtoken` app provides both Django (from v1.7) and South database migrations. See [Schema migrations](#schema-migrations) below.
+
+---
+
You'll also need to create tokens for your users.
@@ -157,11 +167,19 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
- @receiver(post_save, sender=User)
+ from django.conf import settings
+ from django.contrib.auth import get_user_model
+ from django.db.models.signals import post_save
+ from django.dispatch import receiver
+ from rest_framework.authtoken.models import Token
+
+ @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
+Note that you'll want to ensure you place this code snippet in an installed `models.py` module, or some other location that will be imported by Django on startup.
+
If you've already created some users, you can generate tokens for all existing users like this:
from django.contrib.auth.models import User
@@ -172,9 +190,10 @@ If you've already created some users, you can generate tokens for all existing u
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
- urlpatterns += patterns('',
- url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
- )
+ from rest_framework.authtoken import views
+ urlpatterns += [
+ url(r'^api-token-auth/', views.obtain_auth_token)
+ ]
Note that the URL part of the pattern can be whatever you want to use.
@@ -186,7 +205,14 @@ Note that the default `obtain_auth_token` view explicitly uses JSON requests and
#### Schema migrations
-The `rest_framework.authtoken` app includes a south migration that will create the authtoken table.
+The `rest_framework.authtoken` app includes both Django native migrations (for Django versions >1.7) and South migrations (for Django versions <1.7) that will create the authtoken table.
+
+----
+
+**Note**: From REST Framework v2.4.0 using South with Django <1.7 requires upgrading South v1.0+
+
+----
+
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.
@@ -197,13 +223,13 @@ You can do so by inserting a `needed_by` attribute in your user migration:
needed_by = (
('authtoken', '0001_initial'),
)
-
+
def forwards(self):
...
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
@@ -222,150 +248,114 @@ Unauthenticated responses that are denied permission will result in an `HTTP 403
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `PATCH`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
-## OAuthAuthentication
-
-This authentication uses [OAuth 1.0a][oauth-1.0a] authentication scheme. OAuth 1.0a provides signature validation which provides a reasonable level of security over plain non-HTTPS connections. However, it may also be considered more complicated than OAuth2, as it requires clients to sign their requests.
-
-This authentication class depends on the optional `django-oauth-plus` and `oauth2` packages. In order to make it work you must install these packages and add `oauth_provider` to your `INSTALLED_APPS`:
-
- INSTALLED_APPS = (
- ...
- `oauth_provider`,
- )
-
-Don't forget to run `syncdb` once you've added the package.
-
- python manage.py syncdb
-
-#### Getting started with django-oauth-plus
-
-The OAuthAuthentication class only provides token verification and signature validation for requests. It doesn't provide authorization flow for your clients. You still need to implement your own views for accessing and authorizing tokens.
+# Custom authentication
-The `django-oauth-plus` package provides simple foundation for classic 'three-legged' oauth flow. Please refer to [the documentation][django-oauth-plus] for more details.
+To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
-## OAuth2Authentication
+In some circumstances instead of returning `None`, you may want to raise an `AuthenticationFailed` exception from the `.authenticate()` method.
-This authentication uses [OAuth 2.0][rfc6749] authentication scheme. OAuth2 is more simple to work with than OAuth1, and provides much better security than simple token authentication. It is an unauthenticated scheme, and requires you to use an HTTPS connection.
+Typically the approach you should take is:
-This authentication class depends on the optional [django-oauth2-provider][django-oauth2-provider] project. In order to make it work you must install this package and add `provider` and `provider.oauth2` to your `INSTALLED_APPS`:
+* If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked.
+* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes.
- INSTALLED_APPS = (
- ...
- 'provider',
- 'provider.oauth2',
- )
+You *may* also override the `.authenticate_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response.
-You must also include the following in your root `urls.py` module:
+If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
- url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
+## Example
-Note that the `namespace='oauth2'` argument is required.
+The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
-Finally, sync your database.
+ from django.contrib.auth.models import User
+ from rest_framework import authentication
+ from rest_framework import exceptions
- python manage.py syncdb
- python manage.py migrate
+ class ExampleAuthentication(authentication.BaseAuthentication):
+ def authenticate(self, request):
+ username = request.META.get('X_USERNAME')
+ if not username:
+ return None
----
+ try:
+ user = User.objects.get(username=username)
+ except User.DoesNotExist:
+ raise exceptions.AuthenticationFailed('No such user')
-**Note:** If you use `OAuth2Authentication` in production you must ensure that your API is only available over `https`.
+ return (user, None)
---
-#### Getting started with django-oauth2-provider
-
-The `OAuth2Authentication` class only provides token verification for requests. It doesn't provide authorization flow for your clients.
-
-The OAuth 2 authorization flow is taken care by the [django-oauth2-provider][django-oauth2-provider] dependency. A walkthrough is given here, but for more details you should refer to [the documentation][django-oauth2-provider-docs].
-
-To get started:
-
-##### 1. Create a client
-
-You can create a client, either through the shell, or by using the Django admin.
-
-Go to the admin panel and create a new `Provider.Client` entry. It will create the `client_id` and `client_secret` properties for you.
-
-##### 2. Request an access token
-
-To request an access token, submit a `POST` request to the url `/oauth2/access_token` with the following fields:
-
-* `client_id` the client id you've just configured at the previous step.
-* `client_secret` again configured at the previous step.
-* `username` the username with which you want to log in.
-* `password` well, that speaks for itself.
+# Third party packages
-You can use the command line to test that your local configuration is working:
+The following third party packages are also available.
- curl -X POST -d "client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=password&username=YOUR_USERNAME&password=YOUR_PASSWORD" http://localhost:8000/oauth2/access_token/
+## Django OAuth Toolkit
-You should get a response that looks something like this:
+The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excellent [OAuthLib][oauthlib]. The package is well documented, and well supported and is currently our **recommended package for OAuth 2.0 support**.
- {"access_token": "<your-access-token>", "scope": "read", "expires_in": 86399, "refresh_token": "<your-refresh-token>"}
+#### Installation & configuration
-##### 3. Access the API
+Install using `pip`.
-The only thing needed to make the `OAuth2Authentication` class work is to insert the `access_token` you've received in the `Authorization` request header.
+ pip install django-oauth-toolkit
-The command line to test the authentication looks like:
+Add the package to your `INSTALLED_APPS` and modify your REST framework settings.
- curl -H "Authorization: Bearer <your-access-token>" http://localhost:8000/api/
+ INSTALLED_APPS = (
+ ...
+ 'oauth2_provider',
+ )
-### Alternative OAuth 2 implementations
+ REST_FRAMEWORK = {
+ 'DEFAULT_AUTHENTICATION_CLASSES': (
+ 'oauth2_provider.ext.rest_framework.OAuth2Authentication',
+ )
+ }
-Note that [Django OAuth Toolkit][django-oauth-toolkit] is an alternative external package that also includes OAuth 2.0 support for REST framework.
+For more details see the [Django REST framework - Getting started][django-oauth-toolkit-getting-started] documentation.
----
+## Django REST framework OAuth
-# Custom authentication
+The [Django REST framework OAuth][django-rest-framework-oauth] package provides both OAuth1 and OAuth2 support for REST framework.
-To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
+This package was previously included directly in REST framework but is now supported and maintained as a third party package.
-In some circumstances instead of returning `None`, you may want to raise an `AuthenticationFailed` exception from the `.authenticate()` method.
+#### Installation & configuration
-Typically the approach you should take is:
+Install the package using `pip`.
-* If authentication is not attempted, return `None`. Any other authentication schemes also in use will still be checked.
-* If authentication is attempted but fails, raise a `AuthenticationFailed` exception. An error response will be returned immediately, regardless of any permissions checks, and without checking any other authentication schemes.
+ pip install djangorestframework-oauth
-You *may* also override the `.authenticate_header(self, request)` method. If implemented, it should return a string that will be used as the value of the `WWW-Authenticate` header in a `HTTP 401 Unauthorized` response.
+For details on configuration and usage see the Django REST framework OAuth documentation for [authentication][django-rest-framework-oauth-authentication] and [permissions][django-rest-framework-oauth-permissions].
-If the `.authenticate_header()` method is not overridden, the authentication scheme will return `HTTP 403 Forbidden` responses when an unauthenticated request is denied access.
+## Digest Authentication
-## Example
+HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
-The following example will authenticate any incoming request as the user given by the username in a custom request header named 'X_USERNAME'.
+## Django OAuth2 Consumer
- class ExampleAuthentication(authentication.BaseAuthentication):
- def authenticate(self, request):
- username = request.META.get('X_USERNAME')
- if not username:
- return None
+The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
- try:
- user = User.objects.get(username=username)
- except User.DoesNotExist:
- raise exceptions.AuthenticationFailed('No such user')
-
- return (user, None)
+## JSON Web Token Authentication
----
+JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password.
-# Third party packages
+## Hawk HTTP Authentication
-The following third party packages are also available.
+The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let you work with [Hawk][hawk] signed requests and responses in your API. [Hawk][hawk] lets two parties securely communicate with each other using messages signed by a shared key. It is based on [HTTP MAC access authentication][mac] (which was based on parts of [OAuth 1.0][oauth-1.0a]).
-## Digest Authentication
+## HTTP Signature Authentication
-HTTP digest authentication is a widely implemented scheme that was intended to replace HTTP basic authentication, and which provides a simple encrypted authentication mechanism. [Juan Riaza][juanriaza] maintains the [djangorestframework-digestauth][djangorestframework-digestauth] package which provides HTTP digest authentication support for REST framework.
+HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
-## Django OAuth Toolkit
+## Djoser
-The [Django OAuth Toolkit][django-oauth-toolkit] package provides OAuth 2.0 support, and works with Python 2.7 and Python 3.3+. The package is maintained by [Evonove][evonove] and uses the excelllent [OAuthLib][oauthlib]. The package is well documented, and comes as a recommended alternative for OAuth 2.0 support.
+[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system.
-## Django OAuth2 Consumer
+## django-rest-auth
-The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is another package that provides [OAuth 2.0 support for REST framework][doac-rest-framework]. The package includes token scoping permissions on tokens, which allows finer-grained access to your API.
+[Django-rest-auth][django-rest-auth] library provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc. By having these API endpoints, your client apps such as AngularJS, iOS, Android, and others can communicate to your Django backend site independently via REST APIs for user management.
[cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
@@ -374,10 +364,14 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
[oauth]: http://oauth.net/2/
[permission]: permissions.md
[throttling]: throttling.md
-[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
+[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
[mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization
[custom-user-model]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model
[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html
+[django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.org/en/latest/rest-framework/getting_started.html
+[django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/
+[django-rest-framework-oauth-authentication]: http://jpadilla.github.io/django-rest-framework-oauth/authentication/
+[django-rest-framework-oauth-permissions]: http://jpadilla.github.io/django-rest-framework-oauth/permissions/
[juanriaza]: https://github.com/juanriaza
[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
[oauth-1.0a]: http://oauth.net/core/1.0a
@@ -390,4 +384,16 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a
[oauthlib]: https://github.com/idan/oauthlib
[doac]: https://github.com/Rediker-Software/doac
[rediker]: https://github.com/Rediker-Software
-[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/markdown/integrations.md#
+[doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md#
+[blimp]: https://github.com/GetBlimp
+[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
+[etoccalino]: https://github.com/etoccalino/
+[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
+[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html
+[http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/
+[hawkrest]: http://hawkrest.readthedocs.org/en/latest/
+[hawk]: https://github.com/hueniverse/hawk
+[mohawk]: http://mohawk.readthedocs.org/en/latest/
+[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+[djoser]: https://github.com/sunscrapers/djoser
+[django-rest-auth]: https://github.com/Tivix/django-rest-auth
diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md
index 2a774278..bc3b09fb 100644
--- a/docs/api-guide/content-negotiation.md
+++ b/docs/api-guide/content-negotiation.md
@@ -1,4 +1,4 @@
-<a class="github" href="negotiation.py"></a>
+source: negotiation.py
# Content negotiation
@@ -29,7 +29,7 @@ The priorities for each of the given media types would be:
If the requested view was only configured with renderers for `YAML` and `HTML`, then REST framework would select whichever renderer was listed first in the `renderer_classes` list or `DEFAULT_RENDERER_CLASSES` setting.
-For more information on the `HTTP Accept` header, see [RFC 2616][accept-header]
+For more information on the `HTTP Accept` header, see [RFC 2616][accept-header]
---
@@ -54,13 +54,15 @@ The `select_renderer()` method should return a two-tuple of (renderer instance,
The following is a custom content negotiation class which ignores the client
request when selecting the appropriate parser or renderer.
+ from rest_framework.negotiation import BaseContentNegotiation
+
class IgnoreClientContentNegotiation(BaseContentNegotiation):
def select_parser(self, request, parsers):
"""
Select the first parser in the `.parser_classes` list.
"""
return parsers[0]
-
+
def select_renderer(self, request, renderers, format_suffix):
"""
Select the first renderer in the `.renderer_classes` list.
@@ -77,6 +79,10 @@ The default content negotiation class may be set globally, using the `DEFAULT_CO
You can also set the content negotiation used for an individual view, or viewset, using the `APIView` class based views.
+ from myapp.negotiation import IgnoreClientContentNegotiation
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class NoNegotiationView(APIView):
"""
An example view that does not perform content negotiation.
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index 8b3e50f1..3e4b3e8b 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -1,4 +1,4 @@
-<a class="github" href="exceptions.py"></a>
+source: exceptions.py
# Exceptions
@@ -18,7 +18,7 @@ The handled exceptions are:
In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error.
-By default all error responses will include a key `details` in the body of the response, but other keys may also be included.
+Most error responses will include a key `detail` in the body of the response.
For example, the following request:
@@ -28,28 +28,91 @@ For example, the following request:
Might receive an error response indicating that the `DELETE` method is not allowed on that resource:
HTTP/1.1 405 Method Not Allowed
- Content-Type: application/json; charset=utf-8
+ Content-Type: application/json
Content-Length: 42
-
+
{"detail": "Method 'DELETE' not allowed."}
+Validation errors are handled slightly differently, and will include the field names as the keys in the response. If the validation error was not specific to a particular field then it will use the "non_field_errors" key, or whatever string value has been set for the `NON_FIELD_ERRORS_KEY` setting.
+
+Any example validation error might look like this:
+
+ HTTP/1.1 400 Bad Request
+ Content-Type: application/json
+ Content-Length: 94
+
+ {"amount": ["A valid integer is required."], "description": ["This field may not be blank."]}
+
+## Custom exception handling
+
+You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API.
+
+The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response.
+
+For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so:
+
+ HTTP/1.1 405 Method Not Allowed
+ Content-Type: application/json
+ Content-Length: 62
+
+ {"status_code": 405, "detail": "Method 'DELETE' not allowed."}
+
+In order to alter the style of the response, you could write the following custom exception handler:
+
+ from rest_framework.views import exception_handler
+
+ def custom_exception_handler(exc, context):
+ # Call REST framework's default exception handler first,
+ # to get the standard error response.
+ response = exception_handler(exc, context)
+
+ # Now add the HTTP status code to the response.
+ if response is not None:
+ response.data['status_code'] = response.status_code
+
+ return response
+
+The context argument is not used by the default handler, but can be useful if the exception handler needs further information such as the view currently being handled, which can be accessed as `context['view']`.
+
+The exception handler must also be configured in your settings, using the `EXCEPTION_HANDLER` setting key. For example:
+
+ REST_FRAMEWORK = {
+ 'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
+ }
+
+If not specified, the `'EXCEPTION_HANDLER'` setting defaults to the standard exception handler provided by REST framework:
+
+ REST_FRAMEWORK = {
+ 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
+ }
+
+Note that the exception handler will only be called for responses generated by raised exceptions. It will not be used for any responses returned directly by the view, such as the `HTTP_400_BAD_REQUEST` responses that are returned by the generic views when serializer validation fails.
+
---
# API Reference
## APIException
-**Signature:** `APIException(detail=None)`
+**Signature:** `APIException()`
+
+The **base class** for all exceptions raised inside an `APIView` class or `@api_view`.
+
+To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class.
-The **base class** for all exceptions raised inside REST framework.
+For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so:
-To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.
+ from rest_framework.exceptions import APIException
+
+ class ServiceUnavailable(APIException):
+ status_code = 503
+ default_detail = 'Service temporarily unavailable, try again later.'
## ParseError
**Signature:** `ParseError(detail=None)`
-Raised if the request contains malformed data when accessing `request.DATA` or `request.FILES`.
+Raised if the request contains malformed data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "400 Bad Request".
@@ -77,6 +140,14 @@ Raised when an authenticated request fails the permission checks.
By default this exception results in a response with the HTTP status code "403 Forbidden".
+## NotFound
+
+**Signature:** `NotFound(detail=None)`
+
+Raised when a resource does not exists at the given URL. This exception is equivalent to the standard `Http404` Django exception.
+
+By default this exception results in a response with the HTTP status code "404 Not Found".
+
## MethodNotAllowed
**Signature:** `MethodNotAllowed(method, detail=None)`
@@ -85,11 +156,19 @@ Raised when an incoming request occurs that does not map to a handler method on
By default this exception results in a response with the HTTP status code "405 Method Not Allowed".
+## NotAcceptable
+
+**Signature:** `NotAcceptable(detail=None)`
+
+Raised when an incoming request occurs with an `Accept` header that cannot be satisfied by any of the available renderers.
+
+By default this exception results in a response with the HTTP status code "406 Not Acceptable".
+
## UnsupportedMediaType
**Signature:** `UnsupportedMediaType(media_type, detail=None)`
-Raised if there are no parsers that can handle the content type of the request data when accessing `request.DATA` or `request.FILES`.
+Raised if there are no parsers that can handle the content type of the request data when accessing `request.data`.
By default this exception results in a response with the HTTP status code "415 Unsupported Media Type".
@@ -101,5 +180,23 @@ Raised when an incoming request fails the throttling checks.
By default this exception results in a response with the HTTP status code "429 Too Many Requests".
+## ValidationError
+
+**Signature:** `ValidationError(detail)`
+
+The `ValidationError` exception is slightly different from the other `APIException` classes:
+
+* The `detail` argument is mandatory, not optional.
+* The `detail` argument may be a list or dictionary of error details, and may also be a nested data structure.
+* By convention you should import the serializers module and use a fully qualified `ValidationError` style, in order to differentiate it from Django's built-in validation error. For example. `raise serializers.ValidationError('This field must be an integer value.')`
+
+The `ValidationError` class should be used for serializer and field validation, and by validator classes. It is also raised when calling `serializer.is_valid` with the `raise_exception` keyword argument:
+
+ serializer.is_valid(raise_exception=True)
+
+The generic views use the `raise_exception=True` flag, which means that you can override the style of validation error responses globally in your API. To do so, use a custom exception handler, as described above.
+
+By default this exception results in a response with the HTTP status code "400 Bad Request".
+
[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html
[authentication]: authentication.md
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index d69730c9..5edc997a 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -1,8 +1,8 @@
-<a class="github" href="fields.py"></a>
+source: fields.py
# Serializer fields
-> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it &mdash; normalizing it to a consistent format.
+> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it &mdash; normalizing it to a consistent format.
>
> &mdash; [Django documentation][cite]
@@ -10,7 +10,7 @@ Serializer fields handle converting between primitive values and internal dataty
---
-**Note:** The serializer fields are declared in fields.py, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
+**Note:** The serializer fields are declared in `fields.py`, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
---
@@ -18,17 +18,15 @@ Serializer fields handle converting between primitive values and internal dataty
Each serializer field class constructor takes at least these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted:
-### `source`
-
-The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `Field(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `Field(source='user.email')`.
+### `read_only`
-The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations. (See the implementation of the `PaginationSerializer` class for an example.)
+Set this to `True` to ensure that the field is used when serializing a representation, but is not used when creating or updating an instance during deserialization.
-Defaults to the name of the field.
+Defaults to `False`
-### `read_only`
+### `write_only`
-Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
+Set this to `True` to ensure that the field may be used when updating or creating an instance, but is not included when serializing the representation.
Defaults to `False`
@@ -37,27 +35,40 @@ Defaults to `False`
Normally an error will be raised if a field is not supplied during deserialization.
Set to false if this field is not required to be present during deserialization.
+Setting this to `False` also allows the object attribute or dictionary key to be omitted from output when serializing the instance. If the key is not present it will simply not be included in the output representation.
+
Defaults to `True`.
+### `allow_null`
+
+Normally an error will be raised if `None` is passed to a serializer field. Set this keyword argument to `True` if `None` should be considered a valid value.
+
+Defaults to `False`
+
### `default`
-If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
+If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all.
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
+Note that setting a `default` value implies that the field is not required. Including both the `default` and `required` keyword arguments is invalid and will raise an error.
+
+### `source`
+
+The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField('get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
+
+The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
+
+Defaults to the name of the field.
+
### `validators`
-A list of Django validators that should be used to validate deserialized values.
+A list of validator functions which should be applied to the incoming field input, and which either raise a validation error or simply return. Validator functions should typically raise `serializers.ValidationError`, but Django's built-in `ValidationError` is also supported for compatibility with validators defined in the Django codebase or third party Django packages.
### `error_messages`
A dictionary of error codes to error messages.
-### `widget`
-
-Used only if rendering the field to HTML.
-This argument sets the widget that should be used to render the field.
-
### `label`
A short text string that may be used as the name of the field in HTML form fields or other descriptive elements.
@@ -66,158 +77,200 @@ A short text string that may be used as the name of the field in HTML form field
A text string that may be used as a description of the field in HTML form fields or other descriptive elements.
----
+### `initial`
-# Generic Fields
+A value that should be used for pre-populating the value of HTML form fields.
-These generic fields are used for representing arbitrary model fields or the output of model methods.
+### `style`
-## Field
+A dictionary of key-value pairs that can be used to control how renderers should render the field. The API for this should still be considered experimental, and will be formalized with the 3.1 release.
-A generic, **read-only** field. You can use this field for any attribute that does not need to support write operations.
+Two options are currently used in HTML form generation, `'input_type'` and `'base_template'`.
-For example, using the following model.
+ # Use <input type="password"> for the input.
+ password = serializers.CharField(
+ style={'input_type': 'password'}
+ )
- class Account(models.Model):
- owner = models.ForeignKey('auth.user')
- name = models.CharField(max_length=100)
- created = models.DateTimeField(auto_now_add=True)
- payment_expiry = models.DateTimeField()
-
- def has_expired(self):
- now = datetime.datetime.now()
- return now > self.payment_expiry
+ # Use a radio input instead of a select input.
+ color_channel = serializers.ChoiceField(
+ choices=['red', 'green', 'blue']
+ style = {'base_template': 'radio.html'}
+ }
-A serializer definition that looked like this:
+**Note**: The `style` argument replaces the old-style version 2.x `widget` keyword argument. Because REST framework 3 now uses templated HTML form generation, the `widget` option that was used to support Django built-in widgets can no longer be supported. Version 3.1 is planned to include public API support for customizing HTML form generation.
- class AccountSerializer(serializers.HyperlinkedModelSerializer):
- expired = Field(source='has_expired')
-
- class Meta:
- fields = ('url', 'owner', 'name', 'expired')
+---
-Would produce output similar to:
+# Boolean fields
- {
- 'url': 'http://example.com/api/accounts/3/',
- 'owner': 'http://example.com/api/users/12/',
- 'name': 'FooCorp business account',
- 'expired': True
- }
+## BooleanField
-By default, the `Field` class will perform a basic translation of the source value into primitive datatypes, falling back to unicode representations of complex datatypes when necessary.
+A boolean representation.
-You can customize this behavior by overriding the `.to_native(self, value)` method.
+When using HTML encoded form input be aware that omitting a value will always be treated as setting a field to `False`, even if it has a `default=True` option specified. This is because HTML checkbox inputs represent the unchecked state by omitting the value, so REST framework treats omission as if it is an empty checkbox input.
-## WritableField
+Corresponds to `django.db.models.fields.BooleanField`.
-A field that supports both read and write operations. By itself `WritableField` does not perform any translation of input values into a given type. You won't typically use this field directly, but you may want to override it and implement the `.to_native(self, value)` and `.from_native(self, value)` methods.
+**Signature:** `BooleanField()`
-## ModelField
+## NullBooleanField
-A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
+A boolean representation that also accepts `None` as a valid value.
-The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
+Corresponds to `django.db.models.fields.NullBooleanField`.
-**Signature:** `ModelField(model_field=<Django ModelField instance>)`
+**Signature:** `NullBooleanField()`
-## SerializerMethodField
+---
-This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
+# String fields
- from rest_framework import serializers
- from django.contrib.auth.models import User
- from django.utils.timezone import now
+## CharField
- class UserSerializer(serializers.ModelSerializer):
+A text representation. Optionally validates the text to be shorter than `max_length` and longer than `min_length`.
- days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
+Corresponds to `django.db.models.fields.CharField` or `django.db.models.fields.TextField`.
- class Meta:
- model = User
+**Signature:** `CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True)`
- def get_days_since_joined(self, obj):
- return (now() - obj.date_joined).days
+- `max_length` - Validates that the input contains no more than this number of characters.
+- `min_length` - Validates that the input contains no fewer than this number of characters.
+- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
+- `trim_whitespace` - If set to `True` then leading and trailing whitespace is trimmed. Defaults to `True`.
----
+The `allow_null` option is also available for string fields, although its usage is discouraged in favor of `allow_blank`. It is valid to set both `allow_blank=True` and `allow_null=True`, but doing so means that there will be two differing types of empty value permissible for string representations, which can lead to data inconsistencies and subtle application bugs.
-# Typed Fields
+## EmailField
-These fields represent basic datatypes, and support both reading and writing values.
+A text representation, validates the text to be a valid e-mail address.
-## BooleanField
+Corresponds to `django.db.models.fields.EmailField`
-A Boolean representation.
+**Signature:** `EmailField(max_length=None, min_length=None, allow_blank=False)`
-Corresponds to `django.db.models.fields.BooleanField`.
+## RegexField
-## CharField
+A text representation, that validates the given value matches against a certain regular expression.
+
+Corresponds to `django.forms.fields.RegexField`.
-A text representation, optionally validates the text to be shorter than `max_length` and longer than `min_length`.
+**Signature:** `RegexField(regex, max_length=None, min_length=None, allow_blank=False)`
-Corresponds to `django.db.models.fields.CharField`
-or `django.db.models.fields.TextField`.
+The mandatory `regex` argument may either be a string, or a compiled python regular expression object.
+
+Uses Django's `django.core.validators.RegexValidator` for validation.
+
+## SlugField
+
+A `RegexField` that validates the input against the pattern `[a-zA-Z0-9_-]+`.
+
+Corresponds to `django.db.models.fields.SlugField`.
-**Signature:** `CharField(max_length=None, min_length=None)`
+**Signature:** `SlugField(max_length=50, min_length=None, allow_blank=False)`
## URLField
+A `RegexField` that validates the input against a URL matching pattern. Expects fully qualified URLs of the form `http://<host>/<path>`.
+
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
-**Signature:** `CharField(max_length=200, min_length=None)`
+**Signature:** `URLField(max_length=200, min_length=None, allow_blank=False)`
-## SlugField
+## UUIDField
-Corresponds to `django.db.models.fields.SlugField`.
+A field that ensures the input is a valid UUID string. The `to_internal_value` method will return a `uuid.UUID` instance. On output the field will return a string in the canonical hyphenated format, for example:
-**Signature:** `CharField(max_length=50, min_length=None)`
+ "de305d54-75b4-431b-adb2-eb6b9e546013"
-## ChoiceField
+---
-A field that can accept a value out of a limited set of choices.
+# Numeric fields
-## EmailField
+## IntegerField
-A text representation, validates the text to be a valid e-mail address.
+An integer representation.
-Corresponds to `django.db.models.fields.EmailField`
+Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`.
-## RegexField
+**Signature**: `IntegerField(max_value=None, min_value=None)`
-A text representation, that validates the given value matches against a certain regular expression.
+- `max_value` Validate that the number provided is no greater than this value.
+- `min_value` Validate that the number provided is no less than this value.
-Uses Django's `django.core.validators.RegexValidator` for validation.
+## FloatField
+
+A floating point representation.
+
+Corresponds to `django.db.models.fields.FloatField`.
+
+**Signature**: `FloatField(max_value=None, min_value=None)`
+
+- `max_value` Validate that the number provided is no greater than this value.
+- `min_value` Validate that the number provided is no less than this value.
+
+## DecimalField
+
+A decimal representation, represented in Python by a `Decimal` instance.
+
+Corresponds to `django.db.models.fields.DecimalField`.
+
+**Signature**: `DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)`
+
+- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places.
+- `decimal_places` The number of decimal places to store with the number.
+- `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer.
+- `max_value` Validate that the number provided is no greater than this value.
+- `min_value` Validate that the number provided is no less than this value.
+
+#### Example usage
-Corresponds to `django.forms.fields.RegexField`
+To validate numbers up to 999 with a resolution of 2 decimal places, you would use:
-**Signature:** `RegexField(regex, max_length=None, min_length=None)`
+ serializers.DecimalField(max_digits=5, decimal_places=2)
+
+And to validate numbers up to anything less than one billion with a resolution of 10 decimal places:
+
+ serializers.DecimalField(max_digits=19, decimal_places=10)
+
+This field also takes an optional argument, `coerce_to_string`. If set to `True` the representation will be output as a string. If set to `False` the representation will be left as a `Decimal` instance and the final representation will be determined by the renderer.
+
+If unset, this will default to the same value as the `COERCE_DECIMAL_TO_STRING` setting, which is `True` unless set otherwise.
+
+---
+
+# Date and time fields
## DateTimeField
A date and time representation.
-Corresponds to `django.db.models.fields.DateTimeField`
+Corresponds to `django.db.models.fields.DateTimeField`.
-When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default.
+**Signature:** `DateTimeField(format=None, input_formats=None)`
-If you want to override this behavior, you'll need to declare the `DateTimeField` explicitly on the serializer. For example:
+* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. In this case the datetime encoding will be determined by the renderer.
+* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
- class CommentSerializer(serializers.ModelSerializer):
- created = serializers.DateTimeField()
+#### `DateTimeField` format strings.
- class Meta:
- model = Comment
+Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
-Note that by default, datetime representations are determined by the renderer in use, although this can be explicitly overridden as detailed below.
+When a value of `None` is used for the format `datetime` objects will be returned by `to_representation` and the final output representation will determined by the renderer class.
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
-**Signature:** `DateTimeField(format=None, input_formats=None)`
+#### `auto_now` and `auto_now_add` model fields.
-* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
-* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
+When using `ModelSerializer` or `HyperlinkedModelSerializer`, note that any model fields with `auto_now=True` or `auto_now_add=True` will use serializer fields that are `read_only=True` by default.
+
+If you want to override this behavior, you'll need to declare the `DateTimeField` explicitly on the serializer. For example:
+
+ class CommentSerializer(serializers.ModelSerializer):
+ created = serializers.DateTimeField()
-DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
+ class Meta:
+ model = Comment
## DateField
@@ -227,43 +280,64 @@ Corresponds to `django.db.models.fields.DateField`
**Signature:** `DateField(format=None, input_formats=None)`
-* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer.
+* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATE_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `date` objects should be returned by `to_representation`. In this case the date encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
-Date format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
+#### `DateField` format strings
+
+Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
## TimeField
A time representation.
-Optionally takes `format` as parameter to replace the matching pattern.
-
Corresponds to `django.db.models.fields.TimeField`
**Signature:** `TimeField(format=None, input_formats=None)`
-* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer.
+* `format` - A string representing the output format. If not specified, this defaults to the same value as the `TIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `time` objects should be returned by `to_representation`. In this case the time encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
-Time format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
+#### `TimeField` format strings
-## IntegerField
+Format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
-An integer representation.
+---
-Corresponds to `django.db.models.fields.IntegerField`, `django.db.models.fields.SmallIntegerField`, `django.db.models.fields.PositiveIntegerField` and `django.db.models.fields.PositiveSmallIntegerField`
+# Choice selection fields
-## FloatField
+## ChoiceField
-A floating point representation.
+A field that can accept a value out of a limited set of choices.
-Corresponds to `django.db.models.fields.FloatField`.
+Used by `ModelSerializer` to automatically generate fields if the corresponding model field includes a `choices=…` argument.
-## DecimalField
+**Signature:** `ChoiceField(choices)`
-A decimal representation.
+- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
+- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
-Corresponds to `django.db.models.fields.DecimalField`.
+Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
+
+## MultipleChoiceField
+
+A field that can accept a set of zero, one or many values, chosen from a limited set of choices. Takes a single mandatory argument. `to_internal_value` returns a `set` containing the selected values.
+
+**Signature:** `MultipleChoiceField(choices)`
+
+- `choices` - A list of valid values, or a list of `(key, display_name)` tuples.
+- `allow_blank` - If set to `True` then the empty string should be considered a valid value. If set to `False` then the empty string is considered invalid and will raise a validation error. Defaults to `False`.
+
+As with `ChoiceField`, both the `allow_blank` and `allow_null` options are valid, although it is highly recommended that you only use one and not both. `allow_blank` should be preferred for textual choices, and `allow_null` should be preferred for numeric or other non-textual choices.
+
+---
+
+# File upload fields
+
+#### Parsers and file uploads.
+
+The `FileField` and `ImageField` classes are only suitable for use with `MultiPartParser` or `FileUploadParser`. Most parsers, such as e.g. JSON don't support file uploads.
+Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
## FileField
@@ -271,34 +345,145 @@ A file representation. Performs Django's standard FileField validation.
Corresponds to `django.forms.fields.FileField`.
-**Signature:** `FileField(max_length=None, allow_empty_file=False)`
+**Signature:** `FileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)`
- - `max_length` designates the maximum length for the file name.
-
- - `allow_empty_file` designates if empty files are allowed.
+ - `max_length` - Designates the maximum length for the file name.
+ - `allow_empty_file` - Designates if empty files are allowed.
+- `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise.
## ImageField
-An image representation.
+An image representation. Validates the uploaded file content as matching a known image format.
Corresponds to `django.forms.fields.ImageField`.
-Requires the `PIL` package.
+**Signature:** `ImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL)`
+
+ - `max_length` - Designates the maximum length for the file name.
+ - `allow_empty_file` - Designates if empty files are allowed.
+- `use_url` - If set to `True` then URL string values will be used for the output representation. If set to `False` then filename string values will be used for the output representation. Defaults to the value of the `UPLOADED_FILES_USE_URL` settings key, which is `True` unless set otherwise.
-Signature and validation is the same as with `FileField`.
+Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained.
---
-**Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
-Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
+# Composite fields
+
+## ListField
+
+A field class that validates a list of objects.
+
+**Signature**: `ListField(child)`
+
+- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
+
+For example, to validate a list of integers you might use something like the following:
+
+ scores = serializers.ListField(
+ child=serializers.IntegerField(min_value=0, max_value=100)
+ )
+
+The `ListField` class also supports a declarative style that allows you to write reusable list field classes.
+
+ class StringListField(serializers.ListField):
+ child = serializers.CharField()
+
+We can now reuse our custom `StringListField` class throughout our application, without having to provide a `child` argument to it.
+
+## DictField
+
+A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.
+
+**Signature**: `DictField(child)`
+
+- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
+
+For example, to create a field that validates a mapping of strings to strings, you would write something like this:
+
+ document = DictField(child=CharField())
+
+You can also use the declarative style, as with `ListField`. For example:
+
+ class DocumentField(DictField):
+ child = CharField()
+
+---
+
+# Miscellaneous fields
+
+## ReadOnlyField
+
+A field class that simply returns the value of the field without modification.
+
+This field is used by default with `ModelSerializer` when including field names that relate to an attribute rather than a model field.
+
+**Signature**: `ReadOnlyField()`
+
+For example, is `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`:
+
+ class AccountSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Account
+ fields = ('id', 'account_name', 'has_expired')
+
+## HiddenField
+
+A field class that does not take a value based on user input, but instead takes its value from a default value or callable.
+
+**Signature**: `HiddenField()`
+
+For example, to include a field that always provides the current time as part of the serializer validated data, you would use the following:
+
+ modified = serializer.HiddenField(default=timezone.now)
+
+The `HiddenField` class is usually only needed if you have some validation that needs to run based on some pre-provided field values, but you do not want to expose all of those fields to the end user.
+
+For further examples on `HiddenField` see the [validators](validators.md) documentation.
+
+## ModelField
+
+A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to its associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
+
+This field is used by `ModelSerializer` to correspond to custom model field classes.
+
+**Signature:** `ModelField(model_field=<Django ModelField instance>)`
+
+The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
+
+## SerializerMethodField
+
+This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object.
+
+**Signature**: `SerializerMethodField(method_name=None)`
+
+- `method_name` - The name of the method on the serializer to be called. If not included this defaults to `get_<field_name>`.
+
+The serializer method referred to by the `method_name` argument should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
+
+ from django.contrib.auth.models import User
+ from django.utils.timezone import now
+ from rest_framework import serializers
+
+ class UserSerializer(serializers.ModelSerializer):
+ days_since_joined = serializers.SerializerMethodField()
+
+ class Meta:
+ model = User
+
+ def get_days_since_joined(self, obj):
+ return (now() - obj.date_joined).days
---
# Custom fields
-If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
+If you want to create a custom field, you'll need to subclass `Field` and then override either one or both of the `.to_representation()` and `.to_internal_value()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes will typically be any of a number, string, boolean, `date`/`time`/`datetime` or `None`. They may also be any list or dictionary like object that only contains other primitive objects. Other types might be supported, depending on the renderer that you are using.
+
+The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype.
-The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
+The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializer.ValidationError` if the data is invalid.
+
+Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input.
## Examples
@@ -313,32 +498,114 @@ Let's look at an example of serializing a class that represents an RGB color val
assert(red < 256 and green < 256 and blue < 256)
self.red, self.green, self.blue = red, green, blue
- class ColourField(serializers.WritableField):
+ class ColorField(serializers.Field):
"""
- Color objects are serialized into "rgb(#, #, #)" notation.
+ Color objects are serialized into 'rgb(#, #, #)' notation.
"""
- def to_native(self, obj):
+ def to_representation(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
-
- def from_native(self, data):
+
+ def to_internal_value(self, data):
data = data.strip('rgb(').rstrip(')')
red, green, blue = [int(col) for col in data.split(',')]
return Color(red, green, blue)
-
-By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`.
+By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.get_attribute()` and/or `.get_value()`.
As an example, let's create a field that can be used represent the class name of the object being serialized:
class ClassNameField(serializers.Field):
- def field_to_native(self, obj, field_name):
+ def get_attribute(self, obj):
+ # We pass the object instance onto `to_representation`,
+ # not just the field attribute.
+ return obj
+
+ def to_representation(self, obj):
"""
Serialize the object's class name.
"""
- return obj.__class__
+ return obj.__class__.__name__
+
+#### Raising validation errors
+
+Our `ColorField` class above currently does not perform any data validation.
+To indicate invalid data, we should raise a `serializers.ValidationError`, like so:
+
+ def to_internal_value(self, data):
+ if not isinstance(data, six.text_type):
+ msg = 'Incorrect type. Expected a string, but got %s'
+ raise ValidationError(msg % type(data).__name__)
+
+ if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
+ raise ValidationError('Incorrect format. Expected `rgb(#,#,#)`.')
+
+ data = data.strip('rgb(').rstrip(')')
+ red, green, blue = [int(col) for col in data.split(',')]
+
+ if any([col > 255 or col < 0 for col in (red, green, blue)]):
+ raise ValidationError('Value out of range. Must be between 0 and 255.')
+
+ return Color(red, green, blue)
+
+The `.fail()` method is a shortcut for raising `ValidationError` that takes a message string from the `error_messages` dictionary. For example:
+
+ default_error_messages = {
+ 'incorrect_type': 'Incorrect type. Expected a string, but got {input_type}',
+ 'incorrect_format': 'Incorrect format. Expected `rgb(#,#,#)`.',
+ 'out_of_range': 'Value out of range. Must be between 0 and 255.'
+ }
+
+ def to_internal_value(self, data):
+ if not isinstance(data, six.text_type):
+ msg = 'Incorrect type. Expected a string, but got %s'
+ self.fail('incorrect_type', input_type=type(data).__name__)
+
+ if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data):
+ self.fail('incorrect_format')
+
+ data = data.strip('rgb(').rstrip(')')
+ red, green, blue = [int(col) for col in data.split(',')]
+
+ if any([col > 255 or col < 0 for col in (red, green, blue)]):
+ self.fail('out_of_range')
+
+ return Color(red, green, blue)
+
+This style keeps you error messages more cleanly separated from your code, and should be preferred.
+
+# Third party packages
+
+The following third party packages are also available.
+
+## DRF Compound Fields
+
+The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
+
+## DRF Extra Fields
+
+The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes.
+
+## djangrestframework-recursive
+
+the [djangorestframework-recursive][djangorestframework-recursive] package provides a `RecursiveField` for serializing and deserializing recursive structures
+
+## django-rest-framework-gis
+
+The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer.
+
+## django-rest-framework-hstore
+
+The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field.
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
+[django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/
[iso8601]: http://www.w3.org/TR/NOTE-datetime
+[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
+[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields
+[djangorestframework-recursive]: https://github.com/heywbj/django-rest-framework-recursive
+[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis
+[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
+[django-hstore]: https://github.com/djangonauts/django-hstore
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index 05c997a3..b16b6be5 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -1,4 +1,4 @@
-<a class="github" href="filters.py"></a>
+source: filters.py
# Filtering
@@ -20,9 +20,13 @@ You can do so by filtering based on the value of `request.user`.
For example:
- class PurchaseList(generics.ListAPIView)
+ from myapp.models import Purchase
+ from myapp.serializers import PurchaseSerializer
+ from rest_framework import generics
+
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
-
+
def get_queryset(self):
"""
This view should return a list of all the purchases
@@ -34,7 +38,7 @@ For example:
## Filtering against the URL
-Another style of filtering might involve restricting the queryset based on some part of the URL.
+Another style of filtering might involve restricting the queryset based on some part of the URL.
For example if your URL config contained an entry like this:
@@ -42,9 +46,9 @@ For example if your URL config contained an entry like this:
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
- class PurchaseList(generics.ListAPIView)
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
-
+
def get_queryset(self):
"""
This view should return a list of all the purchases for
@@ -53,15 +57,15 @@ You could then write a view that returned a purchase queryset filtered by the us
username = self.kwargs['username']
return Purchase.objects.filter(purchaser__username=username)
-## Filtering against query parameters
+## Filtering against query parameters
A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url.
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
- class PurchaseList(generics.ListAPIView)
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
-
+
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
@@ -90,6 +94,11 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE
You can also set the filter backends on a per-view, or per-viewset basis,
using the `GenericAPIView` class based views.
+ from django.contrib.auth.models import User
+ from myapp.serializers import UserSerializer
+ from rest_framework import filters
+ from rest_framework import generics
+
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
@@ -104,7 +113,7 @@ For instance, given the previous example, and a product with an id of `4675`, th
http://example.com/api/products/4675/?category=clothing&max_price=10.00
## Overriding the initial queryset
-
+
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
class PurchasedProductsList(generics.ListAPIView):
@@ -115,7 +124,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
-
+
def get_queryset(self):
user = self.request.user
return user.purchase_set.all()
@@ -126,7 +135,7 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering
## DjangoFilterBackend
-The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter].
+The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter].
To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
@@ -150,9 +159,14 @@ This will automatically create a `FilterSet` class for the given fields, and wil
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
+ import django_filters
+ from myapp.models import Product
+ from myapp.serializers import ProductSerializer
+ from rest_framework import generics
+
class ProductFilter(django_filters.FilterSet):
- min_price = django_filters.NumberFilter(lookup_type='gte')
- max_price = django_filters.NumberFilter(lookup_type='lte')
+ min_price = django_filters.NumberFilter(name="price", lookup_type='gte')
+ max_price = django_filters.NumberFilter(name="price", lookup_type='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
@@ -162,10 +176,47 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha
serializer_class = ProductSerializer
filter_class = ProductFilter
+
Which will allow you to make requests such as:
http://example.com/api/products?category=clothing&max_price=10.00
+You can also span relationships using `django-filter`, let's assume that each
+product has foreign key to `Manufacturer` model, so we create filter that
+filters using `Manufacturer` name. For example:
+
+ import django_filters
+ from myapp.models import Product
+ from myapp.serializers import ProductSerializer
+ from rest_framework import generics
+
+ class ProductFilter(django_filters.FilterSet):
+ class Meta:
+ model = Product
+ fields = ['category', 'in_stock', 'manufacturer__name']
+
+This enables us to make queries like:
+
+ http://example.com/api/products?manufacturer__name=foo
+
+This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class:
+
+ import django_filters
+ from myapp.models import Product
+ from myapp.serializers import ProductSerializer
+ from rest_framework import generics
+
+ class ProductFilter(django_filters.FilterSet):
+ manufacturer = django_filters.CharFilter(name="manufacturer__name")
+
+ class Meta:
+ model = Product
+ fields = ['category', 'in_stock', 'manufacturer']
+
+And now you can execute:
+
+ http://example.com/api/products?manufacturer=foo
+
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
---
@@ -173,7 +224,7 @@ For more details on using filter sets see the [django-filter documentation][djan
**Hints & Tips**
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
-* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
+* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
@@ -181,9 +232,9 @@ For more details on using filter sets see the [django-filter documentation][djan
## SearchFilter
-The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
+The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
-The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
+The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
@@ -211,13 +262,17 @@ For example:
search_fields = ('=username', '=email')
+By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting.
+
For more details, see the [Django documentation][search-django-admin].
---
## OrderingFilter
-The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example:
+The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting.
+
+For example, to order users by username:
http://example.com/api/users?ordering=username
@@ -229,20 +284,88 @@ Multiple orderings may also be specified:
http://example.com/api/users?ordering=account,username
+### Specifying which fields may be ordered against
+
+It's recommended that you explicitly specify which fields the API should allowing in the ordering filter. You can do this by setting an `ordering_fields` attribute on the view, like so:
+
+ class UserListView(generics.ListAPIView):
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = ('username', 'email')
+
+This helps prevent unexpected data leakage, such as allowing users to order against a password hash field or other sensitive data.
+
+If you *don't* specify an `ordering_fields` attribute on the view, the filter class will default to allowing the user to filter on any readable fields on the serializer specified by the `serializer_class` attribute.
+
+If you are confident that the queryset being used by the view doesn't contain any sensitive data, you can also explicitly specify that a view should allow ordering on *any* model field or queryset aggregate, by using the special value `'__all__'`.
+
+ class BookingsListView(generics.ListAPIView):
+ queryset = Booking.objects.all()
+ serializer_class = BookingSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = '__all__'
+
+### Specifying a default ordering
+
If an `ordering` attribute is set on the view, this will be used as the default ordering.
Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
- serializer = UserSerializer
+ serializer_class = UserSerializer
filter_backends = (filters.OrderingFilter,)
- ordering = ('username',)
+ ordering_fields = ('username', 'email')
+ ordering = ('username',)
The `ordering` attribute may be either a string or a list/tuple of strings.
---
+## DjangoObjectPermissionsFilter
+
+The `DjangoObjectPermissionsFilter` is intended to be used together with the [`django-guardian`][guardian] package, with custom `'view'` permissions added. The filter will ensure that querysets only returns objects for which the user has the appropriate view permission.
+
+This filter class must be used with views that provide either a `queryset` or a `model` attribute.
+
+If you're using `DjangoObjectPermissionsFilter`, you'll probably also want to add an appropriate object permissions class, to ensure that users can only operate on instances if they have the appropriate object permissions. The easiest way to do this is to subclass `DjangoObjectPermissions` and add `'view'` permissions to the `perms_map` attribute.
+
+A complete example using both `DjangoObjectPermissionsFilter` and `DjangoObjectPermissions` might look something like this.
+
+**permissions.py**:
+
+ class CustomObjectPermissions(permissions.DjangoObjectPermissions):
+ """
+ Similar to `DjangoObjectPermissions`, but adding 'view' permissions.
+ """
+ perms_map = {
+ 'GET': ['%(app_label)s.view_%(model_name)s'],
+ 'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
+ 'HEAD': ['%(app_label)s.view_%(model_name)s'],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
+
+**views.py**:
+
+ class EventViewSet(viewsets.ModelViewSet):
+ """
+ Viewset that only lists events if user has 'view' permissions, and only
+ allows operations on individual events if user has appropriate 'view', 'add',
+ 'change' or 'delete' permissions.
+ """
+ queryset = Event.objects.all()
+ serializer = EventSerializer
+ filter_backends = (filters.DjangoObjectPermissionsFilter,)
+ permission_classes = (myapp.permissions.CustomObjectPermissions,)
+
+For more information on adding `'view'` permissions for models, see the [relevant section][view-permissions] of the `django-guardian` documentation, and [this blogpost][view-permissions-blogpost].
+
+---
+
# Custom generic filtering
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
@@ -264,8 +387,20 @@ For example, you might need to restrict users to only being able to see objects
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
+# Third party packages
+
+The following third party packages provide additional filter implementations.
+
+## Django REST framework filters package
+
+The [django-rest-framework-filters package][django-rest-framework-filters] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field.
+
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
+[guardian]: https://django-guardian.readthedocs.org/
+[view-permissions]: https://django-guardian.readthedocs.org/en/latest/userguide/assign.html
+[view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
+[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters
diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md
index 529738e3..35dbcd39 100644
--- a/docs/api-guide/format-suffixes.md
+++ b/docs/api-guide/format-suffixes.md
@@ -1,4 +1,4 @@
-<a class="github" href="urlpatterns.py"></a>
+source: urlpatterns.py
# Format suffixes
@@ -7,7 +7,7 @@ used all the time.
>
> &mdash; Roy Fielding, [REST discuss mailing list][cite]
-A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation.
+A common pattern for Web APIs is to use filename extensions on URLs to provide an endpoint for a given media type. For example, 'http://example.com/api/users.json' to serve a JSON representation.
Adding format-suffix patterns to each individual entry in the URLconf for your API is error-prone and non-DRY, so REST framework provides a shortcut to adding these patterns to your URLConf.
@@ -21,18 +21,19 @@ Arguments:
* **urlpatterns**: Required. A URL pattern list.
* **suffix_required**: Optional. A boolean indicating if suffixes in the URLs should be optional or mandatory. Defaults to `False`, meaning that suffixes are optional by default.
-* **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used.
+* **allowed**: Optional. A list or tuple of valid format suffixes. If not provided, a wildcard format suffix pattern will be used.
Example:
from rest_framework.urlpatterns import format_suffix_patterns
-
- urlpatterns = patterns('blog.views',
- url(r'^/$', 'api_root'),
- url(r'^comments/$', 'comment_list'),
- url(r'^comments/(?P<pk>[0-9]+)/$', 'comment_detail')
- )
-
+ from blog import views
+
+ urlpatterns = [
+ url(r'^/$', views.apt_root),
+ url(r'^comments/$', views.comment_list),
+ url(r'^comments/(?P<pk>[0-9]+)/$', views.comment_detail)
+ ]
+
urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
When using `format_suffix_patterns`, you must make sure to add the `'format'` keyword argument to the corresponding views. For example:
@@ -54,13 +55,25 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
+### Using with `i18n_patterns`
+
+If using the `i18n_patterns` function provided by Django, as well as `format_suffix_patterns` you should make sure that the `i18n_patterns` function is applied as the final, or outermost function. For example:
+
+ url patterns = [
+ …
+ ]
+
+ urlpatterns = i18n_patterns(
+ format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
+ )
+
---
-
+
## Accept headers vs. format suffixes
There seems to be a view among some of the Web community that filename extensions are not a RESTful pattern, and that `HTTP Accept` headers should always be used instead.
-It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators:
+It is actually a misconception. For example, take the following quote from Roy Fielding discussing the relative merits of query parameter media-type indicators vs. file extension media-type indicators:
&ldquo;That's why I always prefer extensions. Neither choice has anything to do with REST.&rdquo; &mdash; Roy Fielding, [REST discuss mailing list][cite2]
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index 67853ed0..7df3d6ff 100755..100644
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -1,5 +1,5 @@
-<a class="github" href="mixins.py"></a>
-<a class="github" href="generics.py"></a>
+source: mixins.py
+ generics.py
# Generic views
@@ -7,7 +7,7 @@
>
> &mdash; [Django Documentation][cite]
-One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
+One of the key benefits of class based views is the way they allow you to compose bits of reusable behavior. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
The generic views provided by REST framework allow you to quickly build API views that map closely to your database models.
@@ -17,6 +17,11 @@ If the generic views don't suit the needs of your API, you can drop down to usin
Typically when using the generic views, you'll override the view, and set several class attributes.
+ from django.contrib.auth.models import User
+ from myapp.serializers import UserSerializer
+ from rest_framework import generics
+ from rest_framework.permissions import IsAdminUser
+
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
@@ -38,9 +43,15 @@ For more complex cases you might also want to override various methods on the vi
return 20
return 100
-For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
+ def list(self, request):
+ # Note the use of `get_queryset()` instead of `self.queryset`
+ queryset = self.get_queryset()
+ serializer = UserSerializer(queryset, many=True)
+ return Response(serializer.data)
+
+For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something like the following entry:
- url(r'^/users/', ListCreateAPIView.as_view(model=User) name='user-list')
+ url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')
---
@@ -58,22 +69,18 @@ Each of the concrete generic views provided is built by combining `GenericAPIVie
The following attributes control the basic view behavior.
-* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
+* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. If you are overriding a view method, it is important that you call `get_queryset()` instead of accessing this property directly, as `queryset` will get evaluated once, and those results will be cached for all subsequent requests.
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
-* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf.
-
-**Shortcuts**:
-
-* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class.
+* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value.
+* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`.
**Pagination**:
-The following attibutes are used to control pagination when used with list views.
+The following attributes are used to control pagination when used with list views.
+
+* `pagination_class` - The pagination class that should be used when paginating list results. Defaults to the same value as the `DEFAULT_PAGINATION_CLASS` setting, which is `'rest_framework.pagination.PageNumberPagination'`.
-* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
-* `paginate_by_param` - The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
-* `pagination_serializer_class` - The pagination serializer class to use when determining the style of paginated responses. Defaults to the same value as the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting.
-* `page_kwarg` - The name of a URL kwarg or URL query parameter which can be used by the client to control which page is requested. Defaults to `'page'`.
+Note that usage of the `paginate_by`, `paginate_by_param` and `page_kwarg` attributes are now pending deprecation. The `pagination_serializer_class` attribute and `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting have been removed completely. Pagination settings should instead be controlled by overriding a pagination class and setting any configuration attributes there. See the pagination documentation for more details.
**Filtering**:
@@ -85,9 +92,11 @@ The following attibutes are used to control pagination when used with list views
#### `get_queryset(self)`
-Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute, or the default queryset for the model if the `model` shortcut is being used.
+Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute.
-May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request.
+This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests.
+
+May be overridden to provide dynamic behavior, such as returning a queryset, that is specific to the user making the request.
For example:
@@ -99,7 +108,7 @@ For example:
Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset.
-May be overridden to provide more complex behavior such as object lookups based on more than one URL kwarg.
+May be overridden to provide more complex behavior, such as object lookups based on more than one URL kwarg.
For example:
@@ -108,13 +117,34 @@ For example:
filter = {}
for field in self.multiple_lookup_fields:
filter[field] = self.kwargs[field]
- return get_object_or_404(queryset, **filter)
+
+ obj = get_object_or_404(queryset, **filter)
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup.
+
+#### `get_filter_backends(self)`
+
+Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute.
+
+May be overridden to provide more complex behavior with filters, such as using different (or even exlusive) lists of filter_backends depending on different criteria.
+
+For example:
+
+ def get_filter_backends(self):
+ if "geo_route" in self.request.QUERY_PARAMS:
+ return (GeoRouteFilter, CategoryFilter)
+ elif "geo_point" in self.request.QUERY_PARAMS:
+ return (GeoPointFilter, CategoryFilter)
+
+ return (CategoryFilter,)
#### `get_serializer_class(self)`
-Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used.
+Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute.
-May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of uesr.
+May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users.
For example:
@@ -125,9 +155,9 @@ For example:
#### `get_paginate_by(self)`
-Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the cient if the `paginate_by_param` attribute is set.
+Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set.
-You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response.
+You may want to override this method to provide more complex behavior, such as modifying page sizes based on the media type of the response.
For example:
@@ -136,29 +166,33 @@ For example:
return 20
return 100
-**Save hooks**:
+**Save and deletion hooks**:
-The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes.
+The following methods are provided by the mixin classes, and provide easy overriding of the object save or deletion behavior.
-* `pre_save(self, obj)` - A hook that is called before saving an object.
-* `post_save(self, obj, created=False)` - A hook that is called after saving an object.
+* `perform_create(self, serializer)` - Called by `CreateModelMixin` when saving a new object instance.
+* `perform_update(self, serializer)` - Called by `UpdateModelMixin` when saving an existing object instance.
+* `perform_destroy(self, instance)` - Called by `DestroyModelMixin` when deleting an object instance.
-The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
+These hooks are particularly useful for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument.
- def pre_save(self, obj):
- """
- Set the object's owner, based on the incoming request.
- """
- obj.owner = self.request.user
+ def perform_create(self, serializer):
+ serializer.save(user=self.request.user)
+
+These override points are also particularly useful for adding behavior that occurs before or after saving an object, such as emailing a confirmation, or logging the update.
+
+ def perform_update(self, serializer):
+ instance = serializer.save()
+ send_email_confirmation(user=self.request.user, modified=instance)
-Remember that the `pre_save()` method is not called by `GenericAPIView` itself, but it is called by `create()` and `update()` methods on the `CreateModelMixin` and `UpdateModelMixin` classes.
+**Note**: These methods replace the old-style version 2.x `pre_save`, `post_save`, `pre_delete` and `post_delete` methods, which are no longer available.
**Other methods**:
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
-* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
+* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
@@ -167,7 +201,9 @@ You won't typically need to override the following methods, although you might n
# Mixins
-The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior.
+The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
+
+The mixin classes can be imported from `rest_framework.mixins`.
## ListModelMixin
@@ -175,8 +211,6 @@ Provides a `.list(request, *args, **kwargs)` method, that implements listing a q
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
-If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
-
## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
@@ -215,6 +249,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
+The view classes can be imported from `rest_framework.generics`.
+
## CreateAPIView
Used for **create-only** endpoints.
@@ -317,7 +353,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap
serializer_class = UserSerializer
lookup_fields = ('account', 'username')
-Using custom mixins is a good option if you have custom behavior that needs to be used
+Using custom mixins is a good option if you have custom behavior that needs to be used.
## Creating custom base classes
@@ -326,18 +362,41 @@ If you are using a mixin across multiple views, you can take this a step further
class BaseRetrieveView(MultipleFieldLookupMixin,
generics.RetrieveAPIView):
pass
-
+
class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin,
generics.RetrieveUpdateDestroyAPIView):
pass
Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project.
-[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
+---
+
+# PUT as create
+
+Prior to version 3.0 the REST framework mixins treated `PUT` as either an update or a create operation, depending on if the object already existed or not.
+
+Allowing `PUT` as create operations is problematic, as it necessarily exposes information about the existence or non-existence of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is necessarily a better default behavior than simply returning `404` responses.
+
+Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but from version 3.0 onwards we now use 404 behavior as the default, due to it being simpler and more obvious.
+If you need to generic PUT-as-create behavior you may want to include something like [this `AllowPUTAsCreateMixin` class](https://gist.github.com/tomchristie/a2ace4577eff2c603b1b) as a mixin to your views.
+
+---
+
+# Third party packages
+
+The following third party packages provide additional generic view implementations.
+
+## Django REST Framework bulk
+
+The [django-rest-framework-bulk package][django-rest-framework-bulk] implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
+
+
+[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
[GenericAPIView]: #genericapiview
[ListModelMixin]: #listmodelmixin
[CreateModelMixin]: #createmodelmixin
[RetrieveModelMixin]: #retrievemodelmixin
[UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin
+[django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk
diff --git a/docs/api-guide/metadata.md b/docs/api-guide/metadata.md
new file mode 100644
index 00000000..01727440
--- /dev/null
+++ b/docs/api-guide/metadata.md
@@ -0,0 +1,103 @@
+source: metadata.py
+
+# Metadata
+
+> [The `OPTIONS`] method allows a client to determine the options and/or requirements associated with a resource, or the capabilities of a server, without implying a resource action or initiating a resource retrieval.
+>
+> &mdash; [RFC7231, Section 4.3.7.][cite]
+
+REST framework includes a configurable mechanism for determining how your API should respond to `OPTIONS` requests. This allows you to return API schema or other resource information.
+
+There are not currently any widely adopted conventions for exactly what style of response should be returned for HTTP `OPTIONS` requests, so we provide an ad-hoc style that returns some useful information.
+
+Here's an example response that demonstrates the information that is returned by default.
+
+ HTTP 200 OK
+ Allow: GET, POST, HEAD, OPTIONS
+ Content-Type: application/json
+
+ {
+ "name": "To Do List",
+ "description": "List existing 'To Do' items, or create a new item.",
+ "renders": [
+ "application/json",
+ "text/html"
+ ],
+ "parses": [
+ "application/json",
+ "application/x-www-form-urlencoded",
+ "multipart/form-data"
+ ],
+ "actions": {
+ "POST": {
+ "note": {
+ "type": "string",
+ "required": false,
+ "read_only": false,
+ "label": "title",
+ "max_length": 100
+ }
+ }
+ }
+ }
+
+## Setting the metadata scheme
+
+You can set the metadata class globally using the `'DEFAULT_METADATA_CLASS'` settings key:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata'
+ }
+
+Or you can set the metadata class individually for a view:
+
+ class APIRoot(APIView):
+ metadata_class = APIRootMetadata
+
+ def get(self, request, format=None):
+ return Response({
+ ...
+ })
+
+The REST framework package only includes a single metadata class implementation, named `SimpleMetadata`. If you want to use an alternative style you'll need to implement a custom metadata class.
+
+## Creating schema endpoints
+
+If you have specific requirements for creating schema endpoints that are accessed with regular `GET` requests, you might consider re-using the metadata API for doing so.
+
+For example, the following additional route could be used on a viewset to provide a linkable schema endpoint.
+
+ @list_route(methods=['GET'])
+ def schema(self, request):
+ meta = self.metadata_class()
+ data = meta.determine_metadata(request, self)
+ return Response(data)
+
+There are a couple of reasons that you might choose to take this approach, including that `OPTIONS` responses [are not cacheable][no-options].
+
+---
+
+# Custom metadata classes
+
+If you want to provide a custom metadata class you should override `BaseMetadata` and implement the `determine_metadata(self, request, view)` method.
+
+Useful things that you might want to do could include returning schema information, using a format such as [JSON schema][json-schema], or returning debug information to admin users.
+
+## Example
+
+The following class could be used to limit the information that is returned to `OPTIONS` requests.
+
+ class MinimalMetadata(BaseMetadata):
+ """
+ Don't include field and other information for `OPTIONS` requests.
+ Just return the name and description.
+ """
+ def determine_metadata(self, request, view):
+ return {
+ 'name': view.get_view_name(),
+ 'description': view.get_view_description()
+ }
+
+[cite]: http://tools.ietf.org/html/rfc7231#section-4.3.7
+[no-options]: https://www.mnot.net/blog/2012/10/29/NO_OPTIONS
+[json-schema]: http://json-schema.org/
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index 912ce41b..eca468b8 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -1,4 +1,4 @@
-<a class="github" href="pagination.py"></a>
+source: pagination.py
# Pagination
@@ -6,138 +6,312 @@
>
> &mdash; [Django documentation][cite]
-REST framework includes a `PaginationSerializer` class that makes it easy to return paginated data in a way that can then be rendered to arbitrary media types.
+REST framework includes support for customizable pagination styles. This allows you to modify how large result sets are split into individual pages of data.
-## Paginating basic data
+The pagination API can support either:
-Let's start by taking a look at an example from the Django documentation.
+* Pagination links that are provided as part of the content of the response.
+* Pagination links that are included in response headers, such as `Content-Range` or `Link`.
- from django.core.paginator import Paginator
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = Paginator(objects, 2)
- page = paginator.page(1)
- page.object_list
- # ['john', 'paul']
+The built-in styles currently all use links included as part of the content of the response. This style is more accessible when using the browsable API.
-At this point we've got a page object. If we wanted to return this page object as a JSON response, we'd need to provide the client with context such as next and previous links, so that it would be able to page through the remaining results.
+Pagination is only performed automatically if you're using the generic views or viewsets. If you're using a regular `APIView`, you'll need to call into the pagination API yourself to ensure you return a paginated response. See the source code for the `mixins.ListMixin` and `generics.GenericAPIView` classes for an example.
- from rest_framework.pagination import PaginationSerializer
- serializer = PaginationSerializer(instance=page)
- serializer.data
- # {'count': 4, 'next': '?page=2', 'previous': None, 'results': [u'john', u'paul']}
+## Setting the pagination style
-The `context` argument of the `PaginationSerializer` class may optionally include the request. If the request is included in the context then the next and previous links returned by the serializer will use absolute URLs instead of relative URLs.
+The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example, to use the built-in limit/offset pagination, you would do:
- request = RequestFactory().get('/foobar')
- serializer = PaginationSerializer(instance=page, context={'request': request})
- serializer.data
- # {'count': 4, 'next': 'http://testserver/foobar?page=2', 'previous': None, 'results': [u'john', u'paul']}
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
+ }
+
+You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis.
+
+## Modifying the pagination style
+
+If you want to modify particular aspects of the pagination style, you'll want to override one of the pagination classes, and set the attributes that you want to change.
+
+ class LargeResultsSetPagination(PageNumberPagination):
+ page_size = 1000
+ page_size_query_param = 'page_size'
+ max_page_size = 10000
+
+ class StandardResultsSetPagination(PageNumberPagination):
+ page_size = 100
+ page_size_query_param = 'page_size'
+ max_page_size = 1000
+
+You can then apply your new style to a view using the `.pagination_class` attribute:
+
+ class BillingRecordsView(generics.ListAPIView):
+ queryset = Billing.objects.all()
+ serializer = BillingRecordsSerializer
+ pagination_class = LargeResultsSetPagination
+
+Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination'
+ }
+
+---
+
+# API Reference
+
+## PageNumberPagination
+
+This pagination style accepts a single number page number in the request query parameters.
+
+**Request**:
+
+ GET https://api.example.org/accounts/?page=4
+
+**Response**:
+
+ HTTP 200 OK
+ {
+ "count": 1023
+ "next": "https://api.example.org/accounts/?page=5",
+ "previous": "https://api.example.org/accounts/?page=3",
+ "results": [
+ …
+ ]
+ }
-We could now return that data in a `Response` object, and it would be rendered into the correct media type.
+#### Setup
-## Paginating QuerySets
+To enable the `PageNumberPagination` style globally, use the following configuration, modifying the `PAGE_SIZE` as desired:
-Our first example worked because we were using primitive objects. If we wanted to paginate a queryset or other complex data, we'd need to specify a serializer to use to serialize the result set itself.
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
+ 'PAGE_SIZE': 100
+ }
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `PageNumberPagination` on a per-view basis.
-We can do this using the `object_serializer_class` attribute on the inner `Meta` class of the pagination serializer. For example.
+#### Configuration
- class UserSerializer(serializers.ModelSerializer):
- """
- Serializes user querysets.
- """
- class Meta:
- model = User
- fields = ('username', 'email')
+The `PageNumberPagination` class includes a number of attributes that may be overridden to modify the pagination style.
- class PaginatedUserSerializer(pagination.PaginationSerializer):
- """
- Serializes page objects of user querysets.
- """
- class Meta:
- object_serializer_class = UserSerializer
+To set these attributes you should override the `PageNumberPagination` class, and then enable your custom pagination class as above.
-We could now use our pagination serializer in a view like this.
+* `page_size` - A numeric value indicating the page size. If set, this overrides the `PAGE_SIZE` setting. Defaults to the same value as the `PAGE_SIZE` settings key.
+* `page_query_param` - A string value indicating the name of the query parameter to use for the pagination control.
+* `page_size_query_param` - If set, this is a string value indicating the name of a query parameter that allows the client to set the page size on a per-request basis. Defaults to `None`, indicating that the client may not control the requested page size.
+* `max_page_size` - If set, this is a numeric value indicating the maximum allowable requested page size. This attribute is only valid if `page_size_query_param` is also set.
+* `last_page_strings` - A list or tuple of string values indicating values that may be used with the `page_query_param` to request the final page in the set. Defaults to `('last',)`
+* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`.
- @api_view('GET')
- def user_list(request):
- queryset = User.objects.all()
- paginator = Paginator(queryset, 20)
+---
+
+## LimitOffsetPagination
- page = request.QUERY_PARAMS.get('page')
- try:
- users = paginator.page(page)
- except PageNotAnInteger:
- # If page is not an integer, deliver first page.
- users = paginator.page(1)
- except EmptyPage:
- # If page is out of range (e.g. 9999),
- # deliver last page of results.
- users = paginator.page(paginator.num_pages)
+This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an
+"offset" query parameter. The limit indicates the maximum number of items to return, and is equivalent to the `page_size` in other styles. The offset indicates the starting position of the query in relation to the complete set of unpaginated items.
- serializer_context = {'request': request}
- serializer = PaginatedUserSerializer(users,
- context=serializer_context)
- return Response(serializer.data)
+**Request**:
-## Pagination in the generic views
+ GET https://api.example.org/accounts/?limit=100&offset=400
-The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
+**Response**:
-The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example.
+ HTTP 200 OK
+ {
+ "count": 1023
+ "next": "https://api.example.org/accounts/?limit=100&offset=500",
+ "previous": "https://api.example.org/accounts/?limit=100&offset=300",
+ "results": [
+ …
+ ]
+ }
+
+#### Setup
+
+To enable the `LimitOffsetPagination` style globally, use the following configuration:
REST_FRAMEWORK = {
- 'PAGINATE_BY': 10,
- 'PAGINATE_BY_PARAM': 'page_size'
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
}
-You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
+Optionally, you may also set a `PAGE_SIZE` key. If the `PAGE_SIZE` parameter is also used then the `limit` query parameter will be optional, and may be omitted by the client.
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `LimitOffsetPagination` on a per-view basis.
- class PaginatedListView(ListAPIView):
- queryset = ExampleModel.objects.all()
- serializer_class = ExampleModelSerializer
- paginate_by = 10
- paginate_by_param = 'page_size'
+#### Configuration
-Note that using a `paginate_by` value of `None` will turn off pagination for the view.
+The `LimitOffsetPagination` class includes a number of attributes that may be overridden to modify the pagination style.
-For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
+To set these attributes you should override the `LimitOffsetPagination` class, and then enable your custom pagination class as above.
+
+* `default_limit` - A numeric value indicating the limit to use if one is not provided by the client in a query parameter. Defaults to the same value as the `PAGE_SIZE` settings key.
+* `limit_query_param` - A string value indicating the name of the "limit" query parameter. Defaults to `'limit'`.
+* `offset_query_param` - A string value indicating the name of the "offset" query parameter. Defaults to `'offset'`.
+* `max_limit` - If set this is a numeric value indicating the maximum allowable limit that may be requested by the client. Defaults to `None`.
+* `template` - The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/numbers.html"`.
---
-# Custom pagination serializers
+## CursorPagination
+
+The cursor-based pagination presents an opaque "cursor" indicator that the client may use to page through the result set. This pagination style only presents forward and reverse controls, and does not allow the client to navigate to arbitrary positions.
-To create a custom pagination serializer class you should override `pagination.BasePaginationSerializer` and set the fields that you want the serializer to return.
+Cursor based pagination requires that there is a unique, unchanging ordering of items in the result set. This ordering might typically be a creation timestamp on the records, as this presents a consistent ordering to paginate against.
-You can also override the name used for the object list field, by setting the `results_field` attribute, which defaults to `'results'`.
+Cursor based pagination is more complex than other schemes. It also requires that the result set presents a fixed ordering, and does not allow the client to arbitrarily index into the result set. However it does provide the following benefits:
+
+* Provides a consistent pagination view. When used properly `CursorPagination` ensures that the client will never see the same item twice when paging through records, even when new items are being inserted by other clients during the pagination process.
+* Supports usage with very large datasets. With extremely large datasets pagination using offset-based pagination styles may become inefficient or unusable. Cursor based pagination schemes instead have fixed-time properties, and do not slow down as the dataset size increases.
+
+#### Details and limitations
+
+Proper use of cursor based pagination requires a little attention to detail. You'll need to think about what ordering you want the scheme to be applied against. The default is to order by `"-created"`. This assumes that **there must be a 'created' timestamp field** on the model instances, and will present a "timeline" style paginated view, with the most recently added items first.
+
+You can modify the ordering by overriding the `'ordering'` attribute on the pagination class, or by using the `OrderingFilter` filter class together with `CursorPagination`. When used with `OrderingFilter` you should strongly consider restricting the fields that the user may order by.
+
+Proper usage of cursor pagination should have an ordering field that satisfies the following:
+
+* Should be an unchanging value, such as a timestamp, slug, or other field that is only set once, on creation.
+* Should be unique, or nearly unique. Millisecond precision timestamps are a good example. This implementation of cursor pagination uses a smart "position plus offset" style that allows it to properly support not-strictly-unique values as the ordering.
+* Should be a non-nullable value that can be coerced to a string.
+* The field should have a database index.
+
+Using an ordering field that does not satisfy these constraints will generally still work, but you'll be loosing some of the benefits of cursor pagination.
+
+For more technical details on the implementation we use for cursor pagination, the ["Building cursors for the Disqus API"][disqus-cursor-api] blog post gives a good overview of the basic approach.
+
+#### Setup
+
+To enable the `CursorPagination` style globally, use the following configuration, modifying the `PAGE_SIZE` as desired:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
+ 'PAGE_SIZE': 100
+ }
+
+On `GenericAPIView` subclasses you may also set the `pagination_class` attribute to select `CursorPagination` on a per-view basis.
+
+#### Configuration
+
+The `CursorPagination` class includes a number of attributes that may be overridden to modify the pagination style.
+
+To set these attributes you should override the `CursorPagination` class, and then enable your custom pagination class as above.
+
+* `page_size` = A numeric value indicating the page size. If set, this overrides the `DEFAULT_PAGE_SIZE` setting. Defaults to the same value as the `DEFAULT_PAGE_SIZE` settings key.
+* `cursor_query_param` = A string value indicating the name of the "cursor" query parameter. Defaults to `'cursor'`.
+* `ordering` = This should be a string, or list of strings, indicating the field against which the cursor based pagination will be applied. For example: `ordering = 'slug'`. Defaults to `-created`. This value may also be overridden by using `OrderingFilter` on the view.
+* `template` = The name of a template to use when rendering pagination controls in the browsable API. May be overridden to modify the rendering style, or set to `None` to disable HTML pagination controls completely. Defaults to `"rest_framework/pagination/previous_and_next.html"`.
+
+---
+
+# Custom pagination styles
+
+To create a custom pagination serializer class you should subclass `pagination.BasePagination` and override the `paginate_queryset(self, queryset, request, view=None)` and `get_paginated_response(self, data)` methods:
+
+* The `paginate_queryset` method is passed the initial queryset and should return an iterable object that contains only the data in the requested page.
+* The `get_paginated_response` method is passed the serialized page data and should return a `Response` instance.
+
+Note that the `paginate_queryset` method may set state on the pagination instance, that may later be used by the `get_paginated_response` method.
## Example
-For example, to nest a pair of links labelled 'prev' and 'next', and set the name for the results field to 'objects', you might use something like this.
+Suppose we want to replace the default pagination output style with a modified format that includes the next and previous links under in a nested 'links' key. We could specify a custom pagination class like so:
+
+ class CustomPagination(pagination.PageNumberPagination):
+ def get_paginated_response(self, data):
+ return Response({
+ 'links': {
+ 'next': self.get_next_link(),
+ 'previous': self.get_previous_link()
+ },
+ 'count': self.page.paginator.count,
+ 'results': data
+ })
+
+We'd then need to setup the custom class in our configuration:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.CustomPagination',
+ 'PAGE_SIZE': 100
+ }
+
+Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional.
+
+## Header based pagination
+
+Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
+
+ class LinkHeaderPagination(pagination.PageNumberPagination):
+ def get_paginated_response(self, data):
+ next_url = self.get_next_link()
+ previous_url = self.get_previous_link()
- class LinksSerializer(serializers.Serializer):
- next = pagination.NextPageField(source='*')
- prev = pagination.PreviousPageField(source='*')
+ if next_url is not None and previous_url is not None:
+ link = '<{next_url}; rel="next">, <{previous_url}; rel="prev">'
+ elif next_url is not None:
+ link = '<{next_url}; rel="next">'
+ elif previous_url is not None:
+ link = '<{previous_url}; rel="prev">'
+ else:
+ link = ''
- class CustomPaginationSerializer(pagination.BasePaginationSerializer):
- links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.Field(source='paginator.count')
+ link = link.format(next_url=next_url, previous_url=previous_url)
+ headers = {'Link': link} if link else {}
- results_field = 'objects'
+ return Response(data, headers=headers)
-## Using your custom pagination serializer
+## Using your custom pagination class
-To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
+To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
REST_FRAMEWORK = {
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
- 'example_app.pagination.CustomPaginationSerializer',
+ 'DEFAULT_PAGINATION_CLASS': 'my_project.apps.core.pagination.LinkHeaderPagination',
+ 'PAGE_SIZE': 100
}
-Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
+API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example:
+
+---
+
+![Link Header][link-header]
+
+*A custom pagination style, using the 'Link' header'*
+
+---
+
+# HTML pagination controls
+
+By default using the pagination classes will cause HTML pagination controls to be displayed in the browsable API. There are two built-in display styles. The `PageNumberPagination` and `LimitOffsetPagination` classes display a list of page numbers with previous and next controls. The `CursorPagination` class displays a simpler style that only displays a previous and next control.
+
+## Customizing the controls
+
+You can override the templates that render the HTML pagination controls. The two built-in styles are:
+
+* `rest_framework/pagination/numbers.html`
+* `rest_framework/pagination/previous_and_next.html`
+
+Providing a template with either of these paths in a global template directory will override the default rendering for the relevant pagination classes.
+
+Alternatively you can disable HTML pagination controls completely by subclassing on of the existing classes, setting `template = None` as an attribute on the class. You'll then need to configure your `DEFAULT_PAGINATION_CLASS` settings key to use your custom class as the default pagination style.
+
+#### Low-level API
+
+The low-level API for determining if a pagination class should display the controls or not is exposed as a `display_page_controls` attribute on the pagination instance. Custom pagination classes should be set to `True` in the `paginate_queryset` method if they require the HTML pagination controls to be displayed.
+
+The `.to_html()` and `.get_html_context()` methods may also be overridden in a custom pagination class in order to further customize how the controls are rendered.
+
+---
+
+# Third party packages
+
+The following third party packages are also available.
+
+## DRF-extensions
- class PaginatedListView(ListAPIView):
- model = ExampleModel
- pagination_serializer_class = CustomPaginationSerializer
- paginate_by = 10
+The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size.
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
+[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
+[link-header]: ../img/link-header-pagination.png
+[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
+[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
+[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/
diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md
index 5bd79a31..c242f878 100644
--- a/docs/api-guide/parsers.md
+++ b/docs/api-guide/parsers.md
@@ -1,4 +1,4 @@
-<a class="github" href="parsers.py"></a>
+source: parsers.py
# Parsers
@@ -12,7 +12,7 @@ REST framework includes a number of built in Parser classes, that allow you to a
## How the parser is determined
-The set of valid parsers for a view is always defined as a list of classes. When either `request.DATA` or `request.FILES` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content.
+The set of valid parsers for a view is always defined as a list of classes. When `request.data` is accessed, REST framework will examine the `Content-Type` header on the incoming request, and determine which parser to use to parse the request content.
---
@@ -26,35 +26,39 @@ As an example, if you are sending `json` encoded data using jQuery with the [.aj
## Setting the parsers
-The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow requests with `YAML` content.
+The default set of parsers may be set globally, using the `DEFAULT_PARSER_CLASSES` setting. For example, the following settings would allow only requests with `JSON` content, instead of the default of JSON or form data.
REST_FRAMEWORK = {
'DEFAULT_PARSER_CLASSES': (
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.JSONParser',
)
}
-You can also set the renderers used for an individual view, or viewset,
+You can also set the parsers used for an individual view, or viewset,
using the `APIView` class based views.
+ from rest_framework.parsers import JSONParser
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
"""
- A view that can accept POST requests with YAML content.
+ A view that can accept POST requests with JSON content.
"""
- parser_classes = (YAMLParser,)
+ parser_classes = (JSONParser,)
def post(self, request, format=None):
- return Response({'received data': request.DATA})
+ return Response({'received data': request.data})
Or, if you're using the `@api_view` decorator with function based views.
@api_view(['POST'])
- @parser_classes((YAMLParser,))
+ @parser_classes((JSONParser,))
def example_view(request, format=None):
"""
- A view that can accept POST requests with YAML content.
+ A view that can accept POST requests with JSON content.
"""
- return Response({'received data': request.DATA})
+ return Response({'received data': request.data})
---
@@ -66,29 +70,9 @@ Parses `JSON` request content.
**.media_type**: `application/json`
-## YAMLParser
-
-Parses `YAML` request content.
-
-Requires the `pyyaml` package to be installed.
-
-**.media_type**: `application/yaml`
-
-## XMLParser
-
-Parses REST framework's default style of `XML` request content.
-
-Note that the `XML` markup language is typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
-
-If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
-
-Requires the `defusedxml` package to be installed.
-
-**.media_type**: `application/xml`
-
## FormParser
-Parses HTML form content. `request.DATA` will be populated with a `QueryDict` of data, `request.FILES` will be populated with an empty `QueryDict` of data.
+Parses HTML form content. `request.data` will be populated with a `QueryDict` of data.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
@@ -96,7 +80,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## MultiPartParser
-Parses multipart HTML form content, which supports file uploads. Both `request.DATA` and `request.FILES` will be populated with a `QueryDict`.
+Parses multipart HTML form content, which supports file uploads. Both `request.data` will be populated with a `QueryDict`.
You will typically want to use both `FormParser` and `MultiPartParser` together in order to fully support HTML form data.
@@ -104,7 +88,7 @@ You will typically want to use both `FormParser` and `MultiPartParser` together
## FileUploadParser
-Parses raw file upload content. The `request.DATA` property will be an empty `QueryDict`, and `request.FILES` will be a dictionary with a single key `'file'` containing the uploaded file.
+Parses raw file upload content. The `request.data` property will be a dictionary with a single key `'file'` containing the uploaded file.
If the view used with `FileUploadParser` is called with a `filename` URL keyword argument, then that argument will be used as the filename. If it is called without a `filename` URL keyword argument, then the client must set the filename in the `Content-Disposition` HTTP header. For example `Content-Disposition: attachment; filename=upload.jpg`.
@@ -122,9 +106,9 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
parser_classes = (FileUploadParser,)
def put(self, request, filename, format=None):
- file_obj = request.FILES['file']
+ file_obj = request.data['file']
# ...
- # do some staff with uploaded file
+ # do some stuff with uploaded file
# ...
return Response(status=204)
@@ -135,7 +119,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword
To implement a custom parser, you should override `BaseParser`, set the `.media_type` property, and implement the `.parse(self, stream, media_type, parser_context)` method.
-The method should return the data that will be used to populate the `request.DATA` property.
+The method should return the data that will be used to populate the `request.data` property.
The arguments passed to `.parse()` are:
@@ -157,7 +141,7 @@ By default this will include the following keys: `view`, `request`, `args`, `kwa
## Example
-The following is an example plaintext parser that will populate the `request.DATA` property with a string representing the body of the request.
+The following is an example plaintext parser that will populate the `request.data` property with a string representing the body of the request.
class PlainTextParser(BaseParser):
"""
@@ -178,13 +162,64 @@ The following is an example plaintext parser that will populate the `request.DAT
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
+## CamelCase JSON
+
+[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
+
[jquery-ajax]: http://api.jquery.com/jQuery.ajax/
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
[juanriaza]: https://github.com/juanriaza
+[vbabiy]: https://github.com/vbabiy
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 2c0a055c..8731cab0 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -1,4 +1,4 @@
-<a class="github" href="permissions.py"></a>
+source: permissions.py
# Permissions
@@ -10,12 +10,24 @@ Together with [authentication] and [throttling], permissions determine whether a
Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
+Permissions are used to grant or deny access different classes of users to different parts of the API.
+
+The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds the `IsAuthenticated` class in REST framework.
+
+A slightly less strict style of permission would be to allow full access to authenticated users, but allow read-only access to unauthenticated users. This corresponds to the `IsAuthenticatedOrReadOnly` class in REST framework.
+
## How permissions are determined
-Permissions in REST framework are always defined as a list of permission classes.
+Permissions in REST framework are always defined as a list of permission classes.
Before running the main body of the view each permission in the list is checked.
-If any permission check fails an `exceptions.PermissionDenied` exception will be raised, and the main body of the view will not run.
+If any permission check fails an `exceptions.PermissionDenied` or `exceptions.NotAuthenticated` exception will be raised, and the main body of the view will not run.
+
+When the permissions checks fail either a "403 Forbidden" or a "401 Unauthorized" response will be returned, according to the following rules:
+
+* The request was successfully authenticated, but permission was denied. *&mdash; An HTTP 403 Forbidden response will be returned.*
+* The request was not successfully authenticated, and the highest priority authentication class *does not* use `WWW-Authenticate` headers. *&mdash; An HTTP 403 Forbidden response will be returned.*
+* The request was not successfully authenticated, and the highest priority authentication class *does* use `WWW-Authenticate` headers. *&mdash; An HTTP 401 Unauthorized response, with an appropriate `WWW-Authenticate` header will be returned.*
## Object level permissions
@@ -25,9 +37,23 @@ Object level permissions are run by REST framework's generic views when `.get_ob
As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object.
If you're writing your own views and want to enforce object level permissions,
-you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
+or if you override the `get_object` method on a generic view, then you'll need to explicitly call the `.check_object_permissions(request, obj)` method on the view at the point at which you've retrieved the object.
+
This will either raise a `PermissionDenied` or `NotAuthenticated` exception, or simply return if the view has the appropriate permissions.
+For example:
+
+ def get_object(self):
+ obj = get_object_or_404(self.get_queryset())
+ self.check_object_permissions(self.request, obj)
+ return obj
+
+#### Limitations of object level permissions
+
+For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
+
+Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view.
+
## Setting the permission policy
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
@@ -47,6 +73,10 @@ If not specified, this setting defaults to allowing unrestricted access:
You can also set the authentication policy on a per-view, or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.permissions import IsAuthenticated
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
permission_classes = (IsAuthenticated,)
@@ -86,7 +116,7 @@ This permission is suitable if you want your API to only be accessible to regist
The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff` is `True` in which case permission will be allowed.
-This permission is suitable is you want your API to only be accessible to a subset of trusted administrators.
+This permission is suitable if you want your API to only be accessible to a subset of trusted administrators.
## IsAuthenticatedOrReadOnly
@@ -96,7 +126,7 @@ This permission is suitable if you want to your API to allow read permissions to
## DjangoModelPermissions
-This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
+This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that has a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
* `POST` requests require the user to have the `add` permission on the model.
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
@@ -106,24 +136,35 @@ The default behaviour can also be overridden to support custom model permissions
To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
+#### Using with views that do not include a `queryset` attribute.
+
+If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sential queryset, so that this class can determine the required permissions. For example:
+
+ queryset = User.objects.none() # Required for DjangoModelPermissions
+
## DjangoModelPermissionsOrAnonReadOnly
-Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API.
+Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API.
+
+## DjangoObjectPermissions
+
+This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian].
-## TokenHasReadWriteScope
+As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned.
-This permission class is intended for use with either of the `OAuthAuthentication` and `OAuth2Authentication` classes, and ties into the scoping that their backends provide.
+* `POST` requests require the user to have the `add` permission on the model instance.
+* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance.
+* `DELETE` requests require the user to have the `delete` permission on the model instance.
-Requests with a safe methods of `GET`, `OPTIONS` or `HEAD` will be allowed if the authenticated token has read permission.
+Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well.
-Requests for `POST`, `PUT`, `PATCH` and `DELETE` will be allowed if the authenticated token has write permission.
+As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details.
-This permission class relies on the implementations of the [django-oauth-plus][django-oauth-plus] and [django-oauth2-provider][django-oauth2-provider] libraries, which both provide limited support for controlling the scope of access tokens:
+---
-* `django-oauth-plus`: Tokens are associated with a `Resource` class which has a `name`, `url` and `is_readonly` properties.
-* `django-oauth2-provider`: Tokens are associated with a bitwise `scope` attribute, that defaults to providing bitwise values for `read` and/or `write`.
+**Note**: If you need object level `view` permissions for `GET`, `HEAD` and `OPTIONS` requests, you'll want to consider also adding the `DjangoObjectPermissionsFilter` class to ensure that list endpoints only return results including objects for which the user has appropriate view permissions.
-If you require more advanced scoping for your API, such as restricting tokens to accessing a subset of functionality of your API then you will need to provide a custom permission class. See the source of the `django-oauth-plus` or `django-oauth2-provider` package for more details on scoping token access.
+---
---
@@ -145,11 +186,7 @@ If you need to test if a request is a read operation or a write operation, you s
---
-**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required.
-
-As of version 2.2 this signature has now been replaced with two separate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
-
-For more details see the [2.2 release announcement][2.2-announcement].
+**Note**: The instance-level `has_object_permission` method will only be called if the view-level `has_permission` checks have already passed. Also note that in order for the instance-level checks to run, the view code should explicitly call `.check_object_permissions(request, obj)`. If you are using the generic views then this will be handled for you by default.
---
@@ -157,6 +194,8 @@ For more details see the [2.2 release announcement][2.2-announcement].
The following is an example of a permission class that checks the incoming request's IP address against a blacklist, and denies the request if the IP has been blacklisted.
+ from rest_framework import permissions
+
class BlacklistPermission(permissions.BasePermission):
"""
Global permission check for blacklisted IPs.
@@ -178,9 +217,9 @@ As well as global permissions, that are run against all incoming requests, you c
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
- if request.method in permissions.SAFE_METHODS:
+ if request.method in permissions.SAFE_METHODS:
return True
-
+
# Instance must have an attribute named `owner`.
return obj.owner == request.user
@@ -188,12 +227,34 @@ Note that the generic views will check the appropriate object level permissions,
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the [filtering documentation][filtering] for more details.
+---
+
+# Third party packages
+
+The following third party packages are also available.
+
+## DRF Any Permissions
+
+The [DRF Any Permissions][drf-any-permissions] packages provides a different permission behavior in contrast to REST framework. Instead of all specified permissions being required, only one of the given permissions has to be true in order to get access to the view.
+
+## Composed Permissions
+
+The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components.
+
+## REST Condition
+
+The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators.
+
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
[authentication]: authentication.md
[throttling]: throttling.md
-[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
+[filtering]: filtering.md
+[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions
+[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions
[guardian]: https://github.com/lukaszb/django-guardian
-[django-oauth-plus]: http://code.larlet.fr/django-oauth-plus
-[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
+[get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user
[2.2-announcement]: ../topics/2.2-announcement.md
[filtering]: filtering.md
+[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
+[composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
+[rest-condition]: https://github.com/caxap/rest_condition
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index 50c9bc54..093bbdd0 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -1,4 +1,4 @@
-<a class="github" href="relations.py"></a>
+source: relations.py
# Serializer relations
@@ -16,6 +16,20 @@ Relational fields are used to represent model relationships. They can be applie
---
+#### Inspecting automatically generated relationships.
+
+When using the `ModelSerializer` class, serializer fields and relationships will be automatically generated for you. Inspecting these automatically generated fields can be a useful tool for determining how to customize the relationship style.
+
+To do so, open the Django shell, using `python manage.py shell`, then import the serializer class, instantiate it, and print the object representation…
+
+ >>> from myapp.serializers import AccountSerializer
+ >>> serializer = AccountSerializer()
+ >>> print repr(serializer) # Or `print(repr(serializer))` in Python 3.x.
+ AccountSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(allow_blank=True, max_length=100, required=False)
+ owner = PrimaryKeyRelatedField(queryset=User.objects.all())
+
# API Reference
In order to explain the various types of relational fields, we'll use a couple of simple models for our examples. Our models will be for music albums, and the tracks listed on each album.
@@ -33,19 +47,19 @@ In order to explain the various types of relational fields, we'll use a couple o
class Meta:
unique_together = ('album', 'order')
order_by = 'order'
-
+
def __unicode__(self):
return '%d: %s' % (self.order, self.title)
-## RelatedField
+## StringRelatedField
-`RelatedField` may be used to represent the target of the relationship using it's `__unicode__` method.
+`StringRelatedField` may be used to represent the target of the relationship using its `__unicode__` method.
For example, the following serializer.
-
+
class AlbumSerializer(serializers.ModelSerializer):
- tracks = RelatedField(many=True)
-
+ tracks = serializers.StringRelatedField(many=True)
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -54,7 +68,7 @@ Would serialize to the following representation.
{
'album_name': 'Things We Lost In The Fire',
- 'artist': 'Low'
+ 'artist': 'Low',
'tracks': [
'1: Sunflower',
'2: Whitetail',
@@ -71,13 +85,13 @@ This field is read only.
## PrimaryKeyRelatedField
-`PrimaryKeyRelatedField` may be used to represent the target of the relationship using it's primary key.
+`PrimaryKeyRelatedField` may be used to represent the target of the relationship using its primary key.
For example, the following serializer:
-
+
class AlbumSerializer(serializers.ModelSerializer):
- tracks = PrimaryKeyRelatedField(many=True, read_only=True)
-
+ tracks = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -86,7 +100,7 @@ Would serialize to a representation like this:
{
'album_name': 'The Roots',
- 'artist': 'Undun'
+ 'artist': 'Undun',
'tracks': [
89,
90,
@@ -99,20 +113,23 @@ By default this field is read-write, although you can change this behavior using
**Arguments**:
+* `queryset` - The queryset used for model instance lookups when validating the field input. Relationships must either set a queryset explicitly, or set `read_only=True`.
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
-* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
-* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
+* `allow_null` - If set to `True`, the field will accept values of `None` or the empty string for nullable relationships. Defaults to `False`.
## HyperlinkedRelatedField
`HyperlinkedRelatedField` may be used to represent the target of the relationship using a hyperlink.
For example, the following serializer:
-
+
class AlbumSerializer(serializers.ModelSerializer):
- tracks = HyperlinkedRelatedField(many=True, read_only=True,
- view_name='track-detail')
-
+ tracks = serializers.HyperlinkedRelatedField(
+ many=True,
+ read_only=True,
+ view_name='track-detail'
+ )
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -121,7 +138,7 @@ Would serialize to a representation like this:
{
'album_name': 'Graceland',
- 'artist': 'Paul Simon'
+ 'artist': 'Paul Simon',
'tracks': [
'http://www.example.com/api/tracks/45/',
'http://www.example.com/api/tracks/46/',
@@ -134,11 +151,12 @@ By default this field is read-write, although you can change this behavior using
**Arguments**:
-* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<modelname>-detail`. **required**.
+* `queryset` - The queryset used for model instance lookups when validating the field input. Relationships must either set a queryset explicitly, or set `read_only=True`.
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
-* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
-* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
+* `allow_null` - If set to `True`, the field will accept values of `None` or the empty string for nullable relationships. Defaults to `False`.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
+* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
## SlugRelatedField
@@ -146,10 +164,14 @@ By default this field is read-write, although you can change this behavior using
`SlugRelatedField` may be used to represent the target of the relationship using a field on the target.
For example, the following serializer:
-
+
class AlbumSerializer(serializers.ModelSerializer):
- tracks = SlugRelatedField(many=True, read_only=True, slug_field='title')
-
+ tracks = serializers.SlugRelatedField(
+ many=True,
+ read_only=True,
+ slug_field='title'
+ )
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -158,7 +180,7 @@ Would serialize to a representation like this:
{
'album_name': 'Dear John',
- 'artist': 'Loney Dear'
+ 'artist': 'Loney Dear',
'tracks': [
'Airport Surroundings',
'Everything Turns to You',
@@ -174,16 +196,16 @@ When using `SlugRelatedField` as a read-write field, you will normally want to e
**Arguments**:
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`. **required**
+* `queryset` - The queryset used for model instance lookups when validating the field input. Relationships must either set a queryset explicitly, or set `read_only=True`.
* `many` - If applied to a to-many relationship, you should set this argument to `True`.
-* `required` - If set to `False`, the field will accept values of `None` or the empty-string for nullable relationships.
-* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
+* `allow_null` - If set to `True`, the field will accept values of `None` or the empty string for nullable relationships. Defaults to `False`.
## HyperlinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer. It can also be used for an attribute on the object. For example, the following serializer:
class AlbumSerializer(serializers.HyperlinkedModelSerializer):
- track_listing = HyperlinkedIdentityField(view_name='track-list')
+ track_listing = serializers.HyperlinkedIdentityField(view_name='track-list')
class Meta:
model = Album
@@ -193,7 +215,7 @@ Would serialize to a representation like this:
{
'album_name': 'The Eraser',
- 'artist': 'Thom Yorke'
+ 'artist': 'Thom Yorke',
'track_listing': 'http://www.example.com/api/track_list/12/',
}
@@ -201,8 +223,9 @@ This field is always read-only.
**Arguments**:
-* `view_name` - The view name that should be used as the target of the relationship. **required**.
+* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<model_name>-detail`. **required**.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
+* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
---
@@ -213,8 +236,6 @@ Nested relationships can be expressed by using serializers as fields.
If the field is used to represent a to-many relationship, you should add the `many=True` flag to the serializer field.
-Note that nested relationships are currently read-only. For read-write relationships, you should use a flat relational style.
-
## Example
For example, the following serializer:
@@ -223,10 +244,10 @@ For example, the following serializer:
class Meta:
model = Track
fields = ('order', 'title')
-
+
class AlbumSerializer(serializers.ModelSerializer):
- tracks = TrackSerializer(many=True)
-
+ tracks = TrackSerializer(many=True, read_only=True)
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -235,7 +256,7 @@ Would serialize to a nested representation like this:
{
'album_name': 'The Grey Album',
- 'artist': 'Danger Mouse'
+ 'artist': 'Danger Mouse',
'tracks': [
{'order': 1, 'title': 'Public Service Announcement'},
{'order': 2, 'title': 'What More Can I Say'},
@@ -246,24 +267,24 @@ Would serialize to a nested representation like this:
# Custom relational fields
-To implement a custom relational field, you should override `RelatedField`, and implement the `.to_native(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target.
+To implement a custom relational field, you should override `RelatedField`, and implement the `.to_representation(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target. The `value` argument will typically be a model instance.
-If you want to implement a read-write relational field, you must also implement the `.from_native(self, data)` method, and add `read_only = False` to the class definition.
+If you want to implement a read-write relational field, you must also implement the `.to_internal_value(self, data)` method.
## Example
-For, example, we could define a relational field, to serialize a track to a custom string representation, using it's ordering, title, and duration.
+For, example, we could define a relational field, to serialize a track to a custom string representation, using its ordering, title, and duration.
import time
class TrackListingField(serializers.RelatedField):
- def to_native(self, value):
+ def to_representation(self, value):
duration = time.strftime('%M:%S', time.gmtime(value.duration))
return 'Track %d: %s (%s)' % (value.order, value.name, duration)
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackListingField(many=True)
-
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -272,7 +293,7 @@ This custom field would then serialize to the following representation.
{
'album_name': 'Sometimes I Wish We Were an Eagle',
- 'artist': 'Bill Callahan'
+ 'artist': 'Bill Callahan',
'tracks': [
'Track 1: Jim Cain (04:39)',
'Track 2: Eid Ma Clack Shaw (04:19)',
@@ -285,6 +306,16 @@ This custom field would then serialize to the following representation.
# Further notes
+## The `queryset` argument
+
+The `queryset` argument is only ever required for *writable* relationship field, in which case it is used for performing the model instance lookup, that maps from the primitive user input, into a model instance.
+
+In version 2.x a serializer class could *sometimes* automatically determine the `queryset` argument *if* a `ModelSerializer` class was being used.
+
+This behavior is now replaced with *always* using an explicit `queryset` argument for writable relational fields.
+
+Doing so reduces the amount of hidden 'magic' that `ModelSerializer` provides, makes the behavior of the field more clear, and ensures that it is trivial to move between using the `ModelSerializer` shortcut, or using fully explicit `Serializer` classes.
+
## Reverse relations
Note that reverse relationships are not automatically included by the `ModelSerializer` and `HyperlinkedModelSerializer` classes. To include a reverse relationship, you must explicitly add it to the fields list. For example:
@@ -303,7 +334,7 @@ If you have not set a related name for the reverse relationship, you'll need to
class AlbumSerializer(serializers.ModelSerializer):
class Meta:
- fields = ('track_set', ...)
+ fields = ('track_set', ...)
See the Django documentation on [reverse relationships][reverse-relationships] for more details.
@@ -316,14 +347,14 @@ For example, given the following model for a tag, which has a generic relationsh
class TaggedItem(models.Model):
"""
Tags arbitrary model instances using a generic relation.
-
+
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
"""
tag_name = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
tagged_object = GenericForeignKey('content_type', 'object_id')
-
+
def __unicode__(self):
return self.tag
@@ -351,23 +382,23 @@ We could define a custom field that could be used to serialize tagged instances,
A custom field to use for the `tagged_object` generic relationship.
"""
- def to_native(self, value):
+ def to_representation(self, value):
"""
Serialize tagged objects to a simple textual representation.
- """
+ """
if isinstance(value, Bookmark):
return 'Bookmark: ' + value.url
elif isinstance(value, Note):
return 'Note: ' + value.text
raise Exception('Unexpected type of tagged object')
-If you need the target of the relationship to have a nested representation, you can use the required serializers inside the `.to_native()` method:
+If you need the target of the relationship to have a nested representation, you can use the required serializers inside the `.to_representation()` method:
- def to_native(self, value):
+ def to_representation(self, value):
"""
Serialize bookmark instances using a bookmark serializer,
and note instances using a note serializer.
- """
+ """
if isinstance(value, Bookmark):
serializer = BookmarkSerializer(value)
elif isinstance(value, Note):
@@ -386,13 +417,13 @@ For more information see [the Django documentation on generic relations][generic
By default, relational fields that target a ``ManyToManyField`` with a
``through`` model specified are set to read-only.
-If you exlicitly specify a relational field pointing to a
+If you explicitly specify a relational field pointing to a
``ManyToManyField`` with a through model, be sure to set ``read_only``
to ``True``.
## Advanced Hyperlinked fields
-If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
+If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
There are two methods you'll need to override.
@@ -405,14 +436,13 @@ attributes are not configured to correctly match the URL conf.
#### get_object(self, queryset, view_name, view_args, view_kwargs)
-
This method should the object that corresponds to the matched URL conf arguments.
May raise an `ObjectDoesNotExist` exception.
### Example
-For example, if all your object URLs used both a account and a slug in the the URL to reference the object, you might create a custom field like this:
+For example, if all your object URLs used both a account and a slug in the the URL to reference the object, you might create a custom field like this:
class CustomHyperlinkedField(serializers.HyperlinkedRelatedField):
def get_url(self, obj, view_name, request, format):
@@ -422,28 +452,21 @@ For example, if all your object URLs used both a account and a slug in the the U
def get_object(self, queryset, view_name, view_args, view_kwargs):
account = view_kwargs['account']
slug = view_kwargs['slug']
- return queryset.get(account=account, slug=sug)
+ return queryset.get(account=account, slug=slug)
---
-## Deprecated APIs
-
-The following classes have been deprecated, in favor of the `many=<bool>` syntax.
-They continue to function, but their usage will raise a `PendingDeprecationWarning`, which is silent by default.
-
-* `ManyRelatedField`
-* `ManyPrimaryKeyRelatedField`
-* `ManyHyperlinkedRelatedField`
-* `ManySlugRelatedField`
+# Third Party Packages
-The `null=<bool>` flag has been deprecated in favor of the `required=<bool>` flag. It will continue to function, but will raise a `PendingDeprecationWarning`.
+The following third party packages are also available.
-In the 2.3 release, these warnings will be escalated to a `DeprecationWarning`, which is loud by default.
-In the 2.4 release, these parts of the API will be removed entirely.
+## DRF Nested Routers
-For more details see the [2.2 release announcement][2.2-announcement].
+The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
[cite]: http://lwn.net/Articles/193245/
[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward
+[routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter
[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1
[2.2-announcement]: ../topics/2.2-announcement.md
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index b434efe9..83ded849 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -1,4 +1,4 @@
-<a class="github" href="renderers.py"></a>
+source: renderers.py
# Renderers
@@ -18,11 +18,11 @@ For more information see the documentation on [content negotiation][conneg].
## Setting the renderers
-The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `YAML` as the main media type and also include the self describing API.
+The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CLASSES` setting. For example, the following settings would use `JSON` as the main media type and also include the self describing API.
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
)
}
@@ -30,11 +30,16 @@ The default set of renderers may be set globally, using the `DEFAULT_RENDERER_CL
You can also set the renderers used for an individual view, or viewset,
using the `APIView` class based views.
+ from django.contrib.auth.models import User
+ from rest_framework.renderers import JSONRenderer
+ from rest_framework.response import Response
+ from rest_framework.views import APIView
+
class UserCountView(APIView):
"""
- A view that returns the count of active users, in JSON or JSONp.
+ A view that returns the count of active users in JSON.
"""
- renderer_classes = (JSONRenderer, JSONPRenderer)
+ renderer_classes = (JSONRenderer, )
def get(self, request, format=None):
user_count = User.objects.filter(active=True).count()
@@ -44,10 +49,10 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
@api_view(['GET'])
- @renderer_classes((JSONRenderer, JSONPRenderer))
+ @renderer_classes((JSONRenderer,))
def user_count_view(request, format=None):
"""
- A view that returns the count of active users, in JSON or JSONp.
+ A view that returns the count of active users in JSON.
"""
user_count = User.objects.filter(active=True).count()
content = {'user_count': user_count}
@@ -69,83 +74,24 @@ If your API includes views that can serve both regular webpages and API response
Renders the request data into `JSON`, using utf-8 encoding.
-Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example:
+Note that the default style is to include unicode characters, and render the response using a compact style with no unnecessary whitespace:
- {"unicode black star": "\u2605"}
+ {"unicode black star":"★","value":999}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
- "unicode black star": "\u2605"
+ "unicode black star": "★",
+ "value": 999
}
-**.media_type**: `application/json`
-
-**.format**: `'.json'`
-
-**.charset**: `utf-8`
-
-## UnicodeJSONRenderer
-
-Renders the request data into `JSON`, using utf-8 encoding.
-
-Note that non-ascii characters will not be character escaped. For example:
-
- {"unicode black star": "★"}
-
-The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
-
- {
- "unicode black star": "★"
- }
-
-Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
+The default JSON encoding style can be altered using the `UNICODE_JSON` and `COMPACT_JSON` settings keys.
**.media_type**: `application/json`
**.format**: `'.json'`
-**.charset**: `utf-8`
-
-## JSONPRenderer
-
-Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
-
-The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`.
-
-**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
-
-**.media_type**: `application/javascript`
-
-**.format**: `'.jsonp'`
-
-**.charset**: `utf-8`
-
-## YAMLRenderer
-
-Renders the request data into `YAML`.
-
-Requires the `pyyaml` package to be installed.
-
-**.media_type**: `application/yaml`
-
-**.format**: `'.yaml'`
-
-**.charset**: `utf-8`
-
-## XMLRenderer
-
-Renders REST framework's default style of `XML` response content.
-
-Note that the `XML` markup language is used typically used as the base language for more strictly defined domain-specific languages, such as `RSS`, `Atom`, and `XHTML`.
-
-If you are considering using `XML` for your API, you may want to consider implementing a custom renderer and parser for your specific requirements, and using an existing domain-specific media-type, or creating your own custom XML-based media-type.
-
-**.media_type**: `application/xml`
-
-**.format**: `'.xml'`
-
-**.charset**: `utf-8`
+**.charset**: `None`
## TemplateHTMLRenderer
@@ -162,17 +108,17 @@ The template name is determined by (in order of preference):
An example of a view that uses `TemplateHTMLRenderer`:
- class UserDetail(generics.RetrieveUserAPIView):
+ class UserDetail(generics.RetrieveAPIView):
"""
A view that returns a templated HTML representations of a given user.
"""
queryset = User.objects.all()
renderer_classes = (TemplateHTMLRenderer,)
- def get(self, request, *args, **kwargs)
+ def get(self, request, *args, **kwargs):
self.object = self.get_object()
return Response({'user': self.object}, template_name='user_detail.html')
-
+
You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers.
@@ -193,7 +139,7 @@ An example of a view that uses `TemplateHTMLRenderer`:
@api_view(('GET',))
@renderer_classes((StaticHTMLRenderer,))
- def simple_html_view(request):
+ def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)
@@ -207,6 +153,20 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
See also: `TemplateHTMLRenderer`
+## HTMLFormRenderer
+
+Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages.
+
+Note that the template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely.
+
+**.media_type**: `text/html`
+
+**.format**: `'.form'`
+
+**.charset**: `utf-8`
+
+**.template**: `'rest_framework/form.html'`
+
## BrowsableAPIRenderer
Renders data into HTML for the Browsable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
@@ -217,6 +177,8 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
**.charset**: `utf-8`
+**.template**: `'rest_framework/api.html'`
+
#### 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:
@@ -241,7 +203,7 @@ This renderer is used for rendering HTML multipart form data. **It is not suita
To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
-The method should return a bytestring, which wil be used as the body of the HTTP response.
+The method should return a bytestring, which will be used as the body of the HTTP response.
The arguments passed to the `.render()` method are:
@@ -272,7 +234,7 @@ The following is an example plaintext renderer that will return a response with
class PlainTextRenderer(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
-
+
def render(self, data, media_type=None, renderer_context=None):
return data.encode(self.charset)
@@ -290,12 +252,15 @@ By default renderer classes are assumed to be using the `UTF-8` encoding. To us
Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding.
-If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
+If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set.
+
+In some cases you may also want to set the `render_style` attribute to `'binary'`. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
class JPEGRenderer(renderers.BaseRenderer):
media_type = 'image/jpeg'
format = 'jpg'
charset = None
+ render_style = 'binary'
def render(self, data, media_type=None, renderer_context=None):
return data
@@ -309,7 +274,7 @@ You can do some pretty flexible things using REST framework's renderers. Some e
* Provide either flat or nested representations from the same endpoint, depending on the requested media type.
* Serve both regular HTML webpages, and JSON based API responses from the same endpoints.
* Specify multiple types of HTML representation for API clients to use.
-* Underspecify a renderer's media type, such as using `media_type = 'image/*'`, and use the `Accept` header to vary the encoding of the response.
+* Underspecify a renderer's media type, such as using `media_type = 'image/*'`, and use the `Accept` header to vary the encoding of the response.
## Varying behaviour by media type
@@ -377,28 +342,125 @@ Templates will render with a `RequestContext` which includes the `status_code` a
The following third party packages are also available.
+## YAML
+
+[REST framework YAML][rest-framework-yaml] provides [YAML][yaml] parsing and rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-yaml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_yaml.parsers.YAMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.YAMLRenderer',
+ ),
+ }
+
+## XML
+
+[REST Framework XML][rest-framework-xml] provides a simple informal XML format. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-xml
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_PARSER_CLASSES': (
+ 'rest_framework_xml.parsers.XMLParser',
+ ),
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_xml.renderers.XMLRenderer',
+ ),
+ }
+
+## JSONP
+
+[REST framework JSONP][rest-framework-jsonp] provides JSONP rendering support. It was previously included directly in the REST framework package, and is now instead supported as a third-party package.
+
+---
+
+**Warning**: If you require cross-domain AJAX requests, you should generally be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details.
+
+The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions.
+
+---
+
+#### Installation & configuration
+
+Install using pip.
+
+ $ pip install djangorestframework-jsonp
+
+Modify your REST framework settings.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_RENDERER_CLASSES': (
+ 'rest_framework_yaml.renderers.JSONPRenderer',
+ ),
+ }
+
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
## CSV
-Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
+Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
+
+## UltraJSON
+
+[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package.
+
+## CamelCase JSON
+
+[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
+
+## Pandas (CSV, Excel, PNG)
+
+[Django REST Pandas] provides a serializer and renderers that support additional data processing and output via the [Pandas] DataFrame API. Django REST Pandas includes renderers for Pandas-style CSV files, Excel workbooks (both `.xls` and `.xlsx`), and a number of [other formats]. It is maintained by [S. Andrew Sheppard][sheppard] as part of the [wq Project][wq].
+
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
-[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/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
+[rest-framework-jsonp]: http://jpadilla.github.io/django-rest-framework-jsonp/
+[cors]: http://www.w3.org/TR/cors/
+[cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/
+[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
[messagepack]: http://msgpack.org/
[juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu
+[vbabiy]: https://github.com/vbabiy
+[rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/
+[rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/
+[yaml]: http://www.yaml.org/
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
+[ultrajson]: https://github.com/esnme/ultrajson
+[hzy]: https://github.com/hzy
+[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
+[Django REST Pandas]: https://github.com/wq/django-rest-pandas
+[Pandas]: http://pandas.pydata.org/
+[other formats]: https://github.com/wq/django-rest-pandas#supported-formats
+[sheppard]: https://github.com/sheppard
+[wq]: https://github.com/wq
diff --git a/docs/api-guide/requests.md b/docs/api-guide/requests.md
index 39a34fcf..658a5ffd 100644
--- a/docs/api-guide/requests.md
+++ b/docs/api-guide/requests.md
@@ -1,4 +1,4 @@
-<a class="github" href="request.py"></a>
+source: request.py
# Requests
@@ -14,26 +14,29 @@ REST framework's `Request` class extends the standard `HttpRequest`, adding supp
REST framework's Request objects provide flexible request parsing that allows you to treat requests with JSON data or other media types in the same way that you would normally deal with form data.
-## .DATA
+## .data
-`request.DATA` returns the parsed content of the request body. This is similar to the standard `request.POST` attribute except that:
+`request.data` returns the parsed content of the request body. This is similar to the standard `request.POST` and `request.FILES` attributes except that:
+* It includes all parsed content, including *file and non-file* inputs.
* It supports parsing the content of HTTP methods other than `POST`, meaning that you can access the content of `PUT` and `PATCH` requests.
* It supports REST framework's flexible request parsing, rather than just supporting form data. For example you can handle incoming JSON data in the same way that you handle incoming form data.
For more details see the [parsers documentation].
-## .FILES
+## .query_params
-`request.FILES` returns any uploaded files that may be present in the content of the request body. This is the same as the standard `HttpRequest` behavior, except that the same flexible request parsing is used for `request.DATA`.
+`request.query_params` is a more correctly named synonym for `request.GET`.
-For more details see the [parsers documentation].
+For clarity inside your code, we recommend using `request.query_params` instead of the Django's standard `request.GET`. Doing so will help keep your codebase more correct and obvious - any HTTP method type may include query parameters, not just `GET` requests.
-## .QUERY_PARAMS
+## .DATA and .FILES
+
+The old-style version 2.x `request.DATA` and `request.FILES` attributes are still available, but are now pending deprecation in favor of the unified `request.data` attribute.
-`request.QUERY_PARAMS` is a more correctly named synonym for `request.GET`.
+## .QUERY_PARAMS
-For clarity inside your code, we recommend using `request.QUERY_PARAMS` instead of the usual `request.GET`, as *any* HTTP method type may include query parameters.
+The old-style version 2.x `request.QUERY_PARAMS` attribute is still available, but is now pending deprecation in favor of the more pythonic `request.query_params`.
## .parsers
@@ -43,12 +46,26 @@ You won't typically need to access this property.
---
-**Note:** If a client sends malformed content, then accessing `request.DATA` or `request.FILES` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response.
+**Note:** If a client sends malformed content, then accessing `request.data` may raise a `ParseError`. By default REST framework's `APIView` class or `@api_view` decorator will catch the error and return a `400 Bad Request` response.
If a client sends a request with a content-type that cannot be parsed then a `UnsupportedMediaType` exception will be raised, which by default will be caught and return a `415 Unsupported Media Type` response.
---
+# Content negotiation
+
+The request exposes some properties that allow you to determine the result of the content negotiation stage. This allows you to implement behaviour such as selecting a different serialisation schemes for different media types.
+
+## .accepted_renderer
+
+The renderer instance what was selected by the content negotiation stage.
+
+## .accepted_media_type
+
+A string representing the media type that was accepted by the content negotiation stage.
+
+---
+
# Authentication
REST framework provides flexible, per-request authentication, that gives you the ability to:
@@ -91,7 +108,7 @@ REST framework supports a few browser enhancements such as browser-based `PUT`,
Browser-based `PUT`, `PATCH` and `DELETE` forms are transparently supported.
-For more information see the [browser enhancements documentation].
+For more information see the [browser enhancements documentation].
## .content_type
@@ -101,7 +118,7 @@ You won't typically need to directly access the request's content type, as you'l
If you do need to access the content type of the request you should use the `.content_type` property in preference to using `request.META.get('HTTP_CONTENT_TYPE')`, as it provides transparent support for browser-based non-form content.
-For more information see the [browser enhancements documentation].
+For more information see the [browser enhancements documentation].
## .stream
@@ -111,13 +128,13 @@ You won't typically need to directly access the request's content, as you'll nor
If you do need to access the raw content directly, you should use the `.stream` property in preference to using `request.content`, as it provides transparent support for browser-based non-form content.
-For more information see the [browser enhancements documentation].
+For more information see the [browser enhancements documentation].
---
# Standard HttpRequest attributes
-As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` dictionary is available as normal.
+As REST framework's `Request` extends Django's `HttpRequest`, all the other standard attributes and methods are also available. For example the `request.META` and `request.session` dictionaries are available as normal.
Note that due to implementation reasons the `Request` class does not inherit from `HttpRequest` class, but instead extends the class using composition.
diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md
index 399b7c23..97f31271 100644
--- a/docs/api-guide/responses.md
+++ b/docs/api-guide/responses.md
@@ -1,4 +1,4 @@
-<a class="github" href="response.py"></a>
+source: response.py
# Responses
@@ -24,7 +24,7 @@ Unless you want to heavily customize REST framework for some reason, you should
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any Python primitives.
-The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primative datatypes before creating the `Response` object.
+The renderers used by the `Response` class cannot natively handle complex datatypes such as Django model instances, so you need to serialize the data into primitive datatypes before creating the `Response` object.
You can use REST framework's `Serializer` classes to perform this data serialization, or use your own custom serialization.
@@ -54,7 +54,7 @@ The rendered content of the response. The `.render()` method must have been cal
## .template_name
-The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the reponse.
+The `template_name`, if supplied. Only required if `HTMLRenderer` or some other custom template renderer is the accepted renderer for the response.
## .accepted_renderer
@@ -90,6 +90,6 @@ The `Response` class extends `SimpleTemplateResponse`, and all the usual attribu
As with any other `TemplateResponse`, this method is called to render the serialized data of the response into the final response content. When `.render()` is called, the response content will be set to the result of calling the `.render(data, accepted_media_type, renderer_context)` method on the `accepted_renderer` instance.
You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle.
-
+
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/
[statuscodes]: status-codes.md
diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md
index 19930dc3..71fb83f9 100644
--- a/docs/api-guide/reverse.md
+++ b/docs/api-guide/reverse.md
@@ -1,4 +1,4 @@
-<a class="github" href="reverse.py"></a>
+source: reverse.py
# Returning URLs
@@ -17,7 +17,7 @@ The advantages of doing so are:
REST framework provides two utility functions to make it more simple to return absolute URIs from your Web API.
-There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink it's output for you, which makes browsing the API much easier.
+There's no requirement for you to use them, but if you do then the self-describing API will be able to automatically hyperlink its output for you, which makes browsing the API much easier.
## reverse
@@ -27,13 +27,13 @@ Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except t
You should **include the request as a keyword argument** to the function, for example:
- import datetime
from rest_framework.reverse import reverse
from rest_framework.views import APIView
-
+ from django.utils.timezone import now
+
class APIRootView(APIView):
def get(self, request):
- year = datetime.datetime.now().year
+ year = now().year
data = {
...
'year-summary-url': reverse('year-summary', args=[year], request=request)
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 86582905..222b6cd2 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -1,4 +1,4 @@
-<a class="github" href="routers.py"></a>
+source: routers.py
# Routers
@@ -12,7 +12,9 @@ REST framework adds support for automatic URL routing to Django, and provides yo
## Usage
-Here's an example of a simple URL conf, that uses `DefaultRouter`.
+Here's an example of a simple URL conf, that uses `SimpleRouter`.
+
+ from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
@@ -26,7 +28,7 @@ There are two mandatory arguments to the `register()` method:
Optionally, you may also specify an additional argument:
-* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset.
+* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `queryset` attribute of the viewset, if it has one. Note that if the viewset does not include a `queryset` attribute then you must set `base_name` when registering the viewset.
The example above would generate the following URL patterns:
@@ -35,44 +37,120 @@ The example above would generate the following URL patterns:
* URL pattern: `^accounts/$` Name: `'account-list'`
* URL pattern: `^accounts/{pk}/$` Name: `'account-detail'`
+---
+
+**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part.
+
+Typically you won't *need* to specify the `base_name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have a `.queryset` attribute set. If you try to register that viewset you'll see an error like this:
+
+ 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.queryset' attribute.
+
+This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name.
+
+---
+
+### Using `include` with routers
+
+The `.urls` attribute on a router instance is simply a standard list of URL patterns. There are a number of different styles for how you can include these URLs.
+
+For example, you can append `router.urls` to a list of existing views…
+
+ router = routers.SimpleRouter()
+ router.register(r'users', UserViewSet)
+ router.register(r'accounts', AccountViewSet)
+
+ urlpatterns = [
+ url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
+ ]
+
+ urlpatterns += router.urls
+
+Alternatively you can use Django's `include` function, like so…
+
+ urlpatterns = [
+ url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
+ url(r'^', include(router.urls)),
+ ]
+
+Router URL patterns can also be namespaces.
+
+ urlpatterns = [
+ url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
+ url(r'^api/', include(router.urls, namespace='api')),
+ ]
+
+If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view.
+
### Extra link and actions
-Any methods on the viewset decorated with `@link` or `@action` will also be routed.
-For example, a given method like this on the `UserViewSet` class:
+Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
+For example, given a method like this on the `UserViewSet` class:
- @action(permission_classes=[IsAdminOrIsSelf])
- def set_password(self, request, pk=None):
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import detail_route
+
+ class UserViewSet(ModelViewSet):
...
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
+ def set_password(self, request, pk=None):
+ ...
+
The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
+If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
+
+For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
+
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import detail_route
+
+ class UserViewSet(ModelViewSet):
+ ...
+
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
+ def set_password(self, request, pk=None):
+ ...
+
+The above example would now generate the following URL pattern:
+
+* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
+
+For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
+
# API Guide
## SimpleRouter
-This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@link` or `@action` decorators.
+This router includes routes for the standard set of `list`, `create`, `retrieve`, `update`, `partial_update` and `destroy` actions. The viewset can also mark additional methods to be routed, using the `@detail_route` or `@list_route` decorators.
<table border=1>
<tr><th>URL Style</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
<tr><td rowspan=2>{prefix}/</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr>
+ <tr><td>{prefix}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr>
- <tr><td rowspan=2>{prefix}/{lookup}/{methodname}/</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr>
- <tr><td>POST</td><td>@action decorated method</td></tr>
+ <tr><td>{prefix}/{lookup}/{methodname}/</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr>
</table>
-By default the URLs created by `SimpleRouter` are appending with a trailing slash.
+By default the URLs created by `SimpleRouter` are appended with a trailing slash.
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
router = SimpleRouter(trailing_slash=False)
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
+The router will match lookup values containing any characters except slashes and period characters. For a more restrictive (or lenient) lookup pattern, set the `lookup_value_regex` attribute on the viewset. For example, you can limit the lookup to valid UUIDs:
+
+ class MyModelViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
+ lookup_field = 'my_model_id'
+ lookup_value_regex = '[0-9a-f]{32}'
+
## DefaultRouter
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
@@ -82,21 +160,21 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
<tr><td>[.format]</td><td>GET</td><td>automatically generated root view</td><td>api-root</td></tr></tr>
<tr><td rowspan=2>{prefix}/[.format]</td><td>GET</td><td>list</td><td rowspan=2>{basename}-list</td></tr></tr>
<tr><td>POST</td><td>create</td></tr>
+ <tr><td>{prefix}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@list_route` decorated method</td><td>{basename}-{methodname}</td></tr>
<tr><td rowspan=4>{prefix}/{lookup}/[.format]</td><td>GET</td><td>retrieve</td><td rowspan=4>{basename}-detail</td></tr></tr>
<tr><td>PUT</td><td>update</td></tr>
<tr><td>PATCH</td><td>partial_update</td></tr>
<tr><td>DELETE</td><td>destroy</td></tr>
- <tr><td rowspan=2>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET</td><td>@link decorated method</td><td rowspan=2>{basename}-{methodname}</td></tr>
- <tr><td>POST</td><td>@action decorated method</td></tr>
+ <tr><td>{prefix}/{lookup}/{methodname}/[.format]</td><td>GET, or as specified by `methods` argument</td><td>`@detail_route` decorated method</td><td>{basename}-{methodname}</td></tr>
</table>
-As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
+As with `SimpleRouter` the trailing slashes on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
router = DefaultRouter(trailing_slash=False)
# Custom Routers
-Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
+Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples.
@@ -116,31 +194,122 @@ The arguments to the `Route` named tuple are:
**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view. Note that the `suffix` argument is reserved for identifying the viewset type, used when generating the view name and breadcrumb links.
+## Customizing dynamic routes
+
+You can also customize how the `@list_route` and `@detail_route` decorators are routed.
+To route either or both of these decorators, include a `DynamicListRoute` and/or `DynamicDetailRoute` named tuple in the `.routes` list.
+
+The arguments to `DynamicListRoute` and `DynamicDetailRoute` are:
+
+**url**: A string representing the URL to be routed. May include the same format strings as `Route`, and additionally accepts the `{methodname}` and `{methodnamehyphen}` format strings.
+
+**name**: The name of the URL as used in `reverse` calls. May include the following format strings: `{basename}`, `{methodname}` and `{methodnamehyphen}`.
+
+**initkwargs**: A dictionary of any additional arguments that should be passed when instantiating the view.
+
## Example
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
- class ReadOnlyRouter(SimpleRouter):
+ from rest_framework.routers import Route, DynamicDetailRoute, SimpleRouter
+
+ class CustomReadOnlyRouter(SimpleRouter):
"""
A router for read-only APIs, which doesn't use trailing slashes.
"""
routes = [
- Route(url=r'^{prefix}$',
- mapping={'get': 'list'},
- name='{basename}-list',
- initkwargs={'suffix': 'List'}),
- Route(url=r'^{prefix}/{lookup}$',
- mapping={'get': 'retrieve'},
- name='{basename}-detail',
- initkwargs={'suffix': 'Detail'})
+ Route(
+ url=r'^{prefix}$',
+ mapping={'get': 'list'},
+ name='{basename}-list',
+ initkwargs={'suffix': 'List'}
+ ),
+ Route(
+ url=r'^{prefix}/{lookup}$',
+ mapping={'get': 'retrieve'},
+ name='{basename}-detail',
+ initkwargs={'suffix': 'Detail'}
+ ),
+ DynamicDetailRoute(
+ url=r'^{prefix}/{lookup}/{methodnamehyphen}$',
+ name='{basename}-{methodnamehyphen}',
+ initkwargs={}
+ )
]
-The `SimpleRouter` class provides another example of setting the `.routes` attribute.
+Let's take a look at the routes our `CustomReadOnlyRouter` would generate for a simple viewset.
+
+`views.py`:
+
+ class UserViewSet(viewsets.ReadOnlyModelViewSet):
+ """
+ A viewset that provides the standard actions
+ """
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+ lookup_field = 'username'
+
+ @detail_route()
+ def group_names(self, request):
+ """
+ Returns a list of all the group names that the given
+ user belongs to.
+ """
+ user = self.get_object()
+ groups = user.groups.all()
+ return Response([group.name for group in groups])
+
+`urls.py`:
+
+ router = CustomReadOnlyRouter()
+ router.register('users', UserViewSet)
+ urlpatterns = router.urls
+
+The following mappings would be generated...
+
+<table border=1>
+ <tr><th>URL</th><th>HTTP Method</th><th>Action</th><th>URL Name</th></tr>
+ <tr><td>/users</td><td>GET</td><td>list</td><td>user-list</td></tr>
+ <tr><td>/users/{username}</td><td>GET</td><td>retrieve</td><td>user-detail</td></tr>
+ <tr><td>/users/{username}/group-names</td><td>GET</td><td>group_names</td><td>user-group-names</td></tr>
+</table>
+
+For another example of setting the `.routes` attribute, see the source code for the `SimpleRouter` class.
## Advanced custom routers
-If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should insect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
+If you want to provide totally custom behavior, you can override `BaseRouter` and override the `get_urls(self)` method. The method should inspect the registered viewsets and return a list of URL patterns. The registered prefix, viewset and basename tuples may be inspected by accessing the `self.registry` attribute.
You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router.
+# Third Party Packages
+
+The following third party packages are also available.
+
+## DRF Nested Routers
+
+The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources.
+
+## wq.db
+
+The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration.
+
+ from wq.db.rest import app
+ from myapp.models import MyModel
+
+ app.router.register_model(MyModel)
+
+## DRF-extensions
+
+The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
+
[cite]: http://guides.rubyonrails.org/routing.html
+[route-decorators]: viewsets.md#marking-extra-actions-for-routing
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
+[wq.db]: http://wq.io/wq.db
+[wq.db-router]: http://wq.io/docs/app.py
+[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
+[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
+[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
+[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
+[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index a1f0853e..aad2236f 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -1,4 +1,4 @@
-<a class="github" href="serializers.py"></a>
+source: serializers.py
# Serializers
@@ -10,48 +10,36 @@ will take some serious design work.
Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into `JSON`, `XML` or other content types. Serializers also provide deserialization, allowing parsed data to be converted back into complex types, after first validating the incoming data.
-REST framework's serializers work very similarly to Django's `Form` and `ModelForm` classes. It provides a `Serializer` class which gives you a powerful, generic way to control the output of your responses, as well as a `ModelSerializer` class which provides a useful shortcut for creating serializers that deal with model instances and querysets.
+The serializers in REST framework work very similarly to Django's `Form` and `ModelForm` classes. We provide a `Serializer` class which gives you a powerful, generic way to control the output of your responses, as well as a `ModelSerializer` class which provides a useful shortcut for creating serializers that deal with model instances and querysets.
## Declaring Serializers
Let's start by creating a simple object we can use for example purposes:
+ from datetime import datetime
+
class Comment(object):
def __init__(self, email, content, created=None):
self.email = email
self.content = content
- self.created = created or datetime.datetime.now()
-
+ self.created = created or datetime.now()
+
comment = Comment(email='leila@example.com', content='foo bar')
-We'll declare a serializer that we can use to serialize and deserialize `Comment` objects.
+We'll declare a serializer that we can use to serialize and deserialize data that corresponds to `Comment` objects.
Declaring a serializer looks very similar to declaring a form:
+ from rest_framework import serializers
+
class CommentSerializer(serializers.Serializer):
email = serializers.EmailField()
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
- def restore_object(self, attrs, instance=None):
- """
- Given a dictionary of deserialized field values, either update
- an existing model instance, or create a new model instance.
- """
- if instance is not None:
- instance.email = attrs.get('email', instance.email)
- instance.content = attrs.get('content', instance.content)
- instance.created = attrs.get('created', instance.created)
- return instance
- return Comment(**attrs)
-
-The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
-
-The `restore_object` method is optional, and is only required if we want our serializer to support deserialization into fully fledged object instances. If we don't define this method, then deserializing data will simply return a dictionary of items.
-
## Serializing objects
-We can now use `CommentSerializer` to serialize a comment, or list of comments. Again, using the `Serializer` class looks a lot like using a `Form` class.
+We can now use `CommentSerializer` to serialize a comment, or list of comments. Again, using the `Serializer` class looks a lot like using a `Form` class.
serializer = CommentSerializer(comment)
serializer.data
@@ -59,38 +47,106 @@ We can now use `CommentSerializer` to serialize a comment, or list of comments.
At this point we've translated the model instance into Python native datatypes. To finalise the serialization process we render the data into `json`.
+ from rest_framework.renderers import JSONRenderer
+
json = JSONRenderer().render(serializer.data)
json
# '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}'
## Deserializing objects
-
-Deserialization is similar. First we parse a stream into Python native datatypes...
- stream = StringIO(json)
+Deserialization is similar. First we parse a stream into Python native datatypes...
+
+ from django.utils.six import BytesIO
+ from rest_framework.parsers import JSONParser
+
+ stream = BytesIO(json)
data = JSONParser().parse(stream)
-...then we restore those native datatypes into a fully populated object instance.
+...then we restore those native datatypes into a dictionary of validated data.
serializer = CommentSerializer(data=data)
serializer.is_valid()
# True
- serializer.object
- # <Comment object at 0x10633b2d0>
- >>> serializer.deserialize('json', stream)
+ serializer.validated_data
+ # {'content': 'foo bar', 'email': 'leila@example.com', 'created': datetime.datetime(2012, 08, 22, 16, 20, 09, 822243)}
+
+## Saving instances
+
+If we want to be able to return complete object instances based on the validated data we need to implement one or both of the `.create()` and `update()` methods. For example:
+
+ class CommentSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ content = serializers.CharField(max_length=200)
+ created = serializers.DateTimeField()
-When deserializing data, we can either create a new instance, or update an existing instance.
+ def create(self, validated_data):
+ return Comment(**validated_data)
- serializer = CommentSerializer(data=data) # Create new instance
- serializer = CommentSerializer(comment, data=data) # Update `instance`
+ def update(self, instance, validated_data):
+ instance.email = validated_data.get('email', instance.email)
+ instance.content = validated_data.get('content', instance.content)
+ instance.created = validated_data.get('created', instance.created)
+ return instance
-By default, serializers must be passed values for all required fields or they will throw validation errors. You can use the `partial` argument in order to allow partial updates.
+If your object instances correspond to Django models you'll also want to ensure that these methods save the object to the database. For example, if `Comment` was a Django model, the methods might look like this:
- serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True) # Update `instance` with partial data
+ def create(self, validated_data):
+ return Comment.objects.create(**validated_data)
+
+ def update(self, instance, validated_data):
+ instance.email = validated_data.get('email', instance.email)
+ instance.content = validated_data.get('content', instance.content)
+ instance.created = validated_data.get('created', instance.created)
+ instance.save()
+ return instance
+
+Now when deserializing data, we can call `.save()` to return an object instance, based on the validated data.
+
+ comment = serializer.save()
+
+Calling `.save()` will either create a new instance, or update an existing instance, depending on if an existing instance was passed when instantiating the serializer class:
+
+ # .save() will create a new instance.
+ serializer = CommentSerializer(data=data)
+
+ # .save() will update the existing `comment` instance.
+ serializer = CommentSerializer(comment, data=data)
+
+Both the `.create()` and `.update()` methods are optional. You can implement either neither, one, or both of them, depending on the use-case for your serializer class.
+
+#### Passing additional attributes to `.save()`
+
+Sometimes you'll want your view code to be able to inject additional data at the point of saving the instance. This additional data might include information like the current user, the current time, or anything else that is not part of the request data.
+
+You can do so by including additional keyword arguments when calling `.save()`. For example:
+
+ serializer.save(owner=request.user)
+
+Any additional keyword arguments will be included in the `validated_data` argument when `.create()` or `.update()` are called.
+
+#### Overriding `.save()` directly.
+
+In some cases the `.create()` and `.update()` method names may not be meaningful. For example, in a contact form we may not be creating new instances, but instead sending an email or other message.
+
+In these cases you might instead choose to override `.save()` directly, as being more readable and meaningful.
+
+For example:
+
+ class ContactForm(serializers.Serializer):
+ email = serializers.EmailField()
+ message = serializers.CharField()
+
+ def save(self):
+ email = self.validated_data['email']
+ message = self.validated_data['message']
+ send_email(from=email, message=message)
+
+Note that in the case above we're now having to access the serializer `.validated_data` property directly.
## Validation
-When deserializing data, you always need to call `is_valid()` before attempting to access the deserialized object. If any validation errors occur, the `.errors` property will contain a dictionary representing the resulting error messages. For example:
+When deserializing data, you always need to call `is_valid()` before attempting to access the validated data, or save an object instance. If any validation errors occur, the `.errors` property will contain a dictionary representing the resulting error messages. For example:
serializer = CommentSerializer(data={'email': 'foobar', 'content': 'baz'})
serializer.is_valid()
@@ -98,17 +154,26 @@ When deserializing data, you always need to call `is_valid()` before attempting
serializer.errors
# {'email': [u'Enter a valid e-mail address.'], 'created': [u'This field is required.']}
-Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors.
+Each key in the dictionary will be the field name, and the values will be lists of strings of any error messages corresponding to that field. The `non_field_errors` key may also be present, and will list any general validation errors. The name of the `non_field_errors` key may be customized using the `NON_FIELD_ERRORS_KEY` REST framework setting.
When deserializing a list of items, errors will be returned as a list of dictionaries representing each of the deserialized items.
+#### Raising an exception on invalid data
+
+The `.is_valid()` method takes an optional `raise_exception` flag that will cause it to raise a `serializers.ValidationError` exception if there are validation errors.
+
+These exceptions are automatically dealt with by the default exception handler that REST framework provides, and will return `HTTP 400 Bad Request` responses by default.
+
+ # Return a 400 response if the data was invalid.
+ serializer.is_valid(raise_exception=True)
+
#### Field-level validation
-You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analogous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
+You can specify custom field-level validation by adding `.validate_<field_name>` methods to your `Serializer` subclass. These are similar to the `.clean_<field_name>` methods on Django forms.
-They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided).
+These methods take a single argument, which is the field value that requires validation.
-Your `validate_<fieldname>` methods should either just return the `attrs` dictionary or raise a `ValidationError`. For example:
+Your `validate_<field_name>` methods should return the validated value or raise a `serializers.ValidationError`. For example:
from rest_framework import serializers
@@ -116,18 +181,17 @@ Your `validate_<fieldname>` methods should either just return the `attrs` dictio
title = serializers.CharField(max_length=100)
content = serializers.CharField()
- def validate_title(self, attrs, source):
+ def validate_title(self, value):
"""
Check that the blog post is about Django.
"""
- value = attrs[source]
- if "django" not in value.lower():
+ if 'django' not in value.lower():
raise serializers.ValidationError("Blog post is not about Django")
- return attrs
+ return value
#### Object-level validation
-To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. For example:
+To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is a dictionary of field values. It should raise a `ValidationError` if necessary, or just return the validated values. For example:
from rest_framework import serializers
@@ -136,24 +200,54 @@ To do any other validation that requires access to multiple fields, add a method
start = serializers.DateTimeField()
finish = serializers.DateTimeField()
- def validate(self, attrs):
+ def validate(self, data):
"""
Check that the start is before the stop.
"""
- if attrs['start'] < attrs['finish']:
+ if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
- return attrs
+ return data
+
+#### Validators
+
+Individual fields on a serializer can include validators, by declaring them on the field instance, for example:
+
+ def multiple_of_ten(value):
+ if value % 10 != 0:
+ raise serializers.ValidationError('Not a multiple of ten')
+
+ class GameRecord(serializers.Serializer):
+ score = IntegerField(validators=[multiple_of_ten])
+ ...
+
+Serializer classes can also include reusable validators that are applied to the complete set of field data. These validators are included by declaring them on an inner `Meta` class, like so:
+
+ class EventSerializer(serializers.Serializer):
+ name = serializers.CharField()
+ room_number = serializers.IntegerField(choices=[101, 102, 103, 201])
+ date = serializers.DateField()
+
+ class Meta:
+ # Each room only has one event per day.
+ validators = UniqueTogetherValidator(
+ queryset=Event.objects.all(),
+ fields=['room_number', 'date']
+ )
+
+For more information see the [validators documentation](validators.md).
-## Saving object state
+## Accessing the initial data and instance
-To save the deserialized objects created by a serializer, call the `.save()` method:
+When passing an initial object or queryset to a serializer instance, the object will be made available as `.instance`. If no initial object is passed then the `.instance` attribute will be `None`.
- if serializer.is_valid():
- serializer.save()
+When passing data to a serializer instance, the unmodified data will be made available as `.initial_data`. If the data keyword argument is not passed then the `.initial_data` attribute will not exist.
-The default behavior of the method is to simply call `.save()` on the deserialized object instance. You can override the default save behaviour by overriding the `.save_object(obj)` method on the serializer class.
+## Partial updates
-The generic views provided by REST framework call the `.save()` method when updating or creating entities.
+By default, serializers must be passed values for all required fields or they will raise validation errors. You can use the `partial` argument in order to allow partial updates.
+
+ # Update `comment` with partial data
+ serializer = CommentSerializer(comment, data={'content': u'foo bar'}, partial=True)
## Dealing with nested objects
@@ -177,7 +271,7 @@ If a nested representation may optionally accept the `None` value you should pas
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
-Similarly if a nested representation should be a list of items, you should the `many=True` flag to the nested serialized.
+Similarly if a nested representation should be a list of items, you should pass the `many=True` flag to the nested serialized.
class CommentSerializer(serializers.Serializer):
user = UserSerializer(required=False)
@@ -185,101 +279,127 @@ Similarly if a nested representation should be a list of items, you should the `
content = serializers.CharField(max_length=200)
created = serializers.DateTimeField()
----
+## Writable nested representations
-**Note**: Nested serializers are only suitable for read-only representations, as there are cases where they would have ambiguous or non-obvious behavior if used when updating instances. For read-write representations you should always use a flat representation, by using one of the `RelatedField` subclasses.
+When dealing with nested representations that support deserializing the data, an errors with nested objects will be nested under the field name of the nested object.
----
+ serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
+ serializer.is_valid()
+ # False
+ serializer.errors
+ # {'user': {'email': [u'Enter a valid e-mail address.']}, 'created': [u'This field is required.']}
-## Dealing with multiple objects
+Similarly, the `.validated_data` property will include nested data structures.
-The `Serializer` class can also handle serializing or deserializing lists of objects.
+#### Writing `.create()` methods for nested representations
-#### Serializing multiple objects
+If you're supporting writable nested representations you'll need to write `.create()` or `.update()` methods that handle saving multiple objects.
-To serialize a queryset or list of objects instead of a single object instance, you should pass the `many=True` flag when instantiating the serializer. You can then pass a queryset or list of objects to be serialized.
+The following example demonstrates how you might handle creating a user with a nested profile object.
- queryset = Book.objects.all()
- serializer = BookSerializer(queryset, many=True)
- serializer.data
- # [
- # {'id': 0, 'title': 'The electric kool-aid acid test', 'author': 'Tom Wolfe'},
- # {'id': 1, 'title': 'If this is a man', 'author': 'Primo Levi'},
- # {'id': 2, 'title': 'The wind-up bird chronicle', 'author': 'Haruki Murakami'}
- # ]
+ class UserSerializer(serializers.ModelSerializer):
+ profile = ProfileSerializer()
-#### Deserializing multiple objects for creation
+ class Meta:
+ model = User
+ fields = ('username', 'email', 'profile')
-To deserialize a list of object data, and create multiple object instances in a single pass, you should also set the `many=True` flag, and pass a list of data to be deserialized.
+ def create(self, validated_data):
+ profile_data = validated_data.pop('profile')
+ user = User.objects.create(**validated_data)
+ Profile.objects.create(user=user, **profile_data)
+ return user
-This allows you to write views that create multiple items when a `POST` request is made.
+#### Writing `.update()` methods for nested representations
-For example:
+For updates you'll want to think carefully about how to handle updates to relationships. For example if the data for the relationship is `None`, or not provided, which of the following should occur?
- data = [
- {'title': 'The bell jar', 'author': 'Sylvia Plath'},
- {'title': 'For whom the bell tolls', 'author': 'Ernest Hemingway'}
- ]
- serializer = BookSerializer(data=data, many=True)
- serializer.is_valid()
- # True
- serializer.save() # `.save()` will be called on each deserialized instance
+* Set the relationship to `NULL` in the database.
+* Delete the associated instance.
+* Ignore the data and leave the instance as it is.
+* Raise a validation error.
-#### Deserializing multiple objects for update
+Here's an example for an `update()` method on our previous `UserSerializer` class.
-You can also deserialize a list of objects as part of a bulk update of multiple existing items.
-In this case you need to supply both an existing list or queryset of items, as well as a list of data to update those items with.
+ def update(self, instance, validated_data):
+ profile_data = validated_data.pop('profile')
+ # Unless the application properly enforces that this field is
+ # always set, the follow could raise a `DoesNotExist`, which
+ # would need to be handled.
+ profile = instance.profile
-This allows you to write views that update or create multiple items when a `PUT` request is made.
+ instance.username = validated_data.get('username', instance.username)
+ instance.email = validated_data.get('email', instance.email)
+ instance.save()
- # Capitalizing the titles of the books
- queryset = Book.objects.all()
- data = [
- {'id': 3, 'title': 'The Bell Jar', 'author': 'Sylvia Plath'},
- {'id': 4, 'title': 'For Whom the Bell Tolls', 'author': 'Ernest Hemingway'}
- ]
- serializer = BookSerializer(queryset, data=data, many=True)
- serializer.is_valid()
- # True
- serialize.save() # `.save()` will be called on each updated or newly created instance.
+ profile.is_premium_member = profile_data.get(
+ 'is_premium_member',
+ profile.is_premium_member
+ )
+ profile.has_support_contract = profile_data.get(
+ 'has_support_contract',
+ profile.has_support_contract
+ )
+ profile.save()
-By default bulk updates will be limited to updating instances that already exist in the provided queryset.
+ return instance
-When performing a bulk update you may want to allow new items to be created, and missing items to be deleted. To do so, pass `allow_add_remove=True` to the serializer.
+Because the behavior of nested creates and updates can be ambiguous, and may require complex dependancies between related models, REST framework 3 requires you to always write these methods explicitly. The default `ModelSerializer` `.create()` and `.update()` methods do not include support for writable nested representations.
- serializer = BookSerializer(queryset, data=data, many=True, allow_add_remove=True)
- serializer.is_valid()
- # True
- serializer.save() # `.save()` will be called on updated or newly created instances.
- # `.delete()` will be called on any other items in the `queryset`.
+It is possible that a third party package, providing automatic support some kinds of automatic writable nested representations may be released alongside the 3.1 release.
-Passing `allow_add_remove=True` ensures that any update operations will completely overwrite the existing queryset, rather than simply updating existing objects.
+#### Handling saving related instances in model manager classes
-#### How identity is determined when performing bulk updates
+An alternative to saving multiple related instances in the serializer is to write custom model manager classes handle creating the correct instances.
-Performing a bulk update is slightly more complicated than performing a bulk creation, because the serializer needs a way to determine how the items in the incoming data should be matched against the existing object instances.
+For example, suppose we wanted to ensure that `User` instances and `Profile` instances are always created together as a pair. We might write a custom manager class that looks something like this:
-By default the serializer class will use the `id` key on the incoming data to determine the canonical identity of an object. If you need to change this behavior you should override the `get_identity` method on the `Serializer` class. For example:
+ class UserManager(models.Manager):
+ ...
- class AccountSerializer(serializers.Serializer):
- slug = serializers.CharField(max_length=100)
- created = serializers.DateTimeField()
- ... # Various other fields
-
- def get_identity(self, data):
- """
- This hook is required for bulk update.
- We need to override the default, to use the slug as the identity.
-
- Note that the data has not yet been validated at this point,
- so we need to deal gracefully with incorrect datatypes.
- """
- try:
- return data.get('slug', None)
- except AttributeError:
- return None
+ def create(self, username, email, is_premium_member=False, has_support_contract=False):
+ user = User(username=username, email=email)
+ user.save()
+ profile = Profile(
+ user=user,
+ is_premium_member=is_premium_member,
+ has_support_contract=has_support_contract
+ )
+ profile.save()
+ return user
+
+This manager class now more nicely encapsulates that user instances and profile instances are always created at the same time. Our `.create()` method on the serializer class can now be re-written to use the new manager method.
+
+ def create(self, validated_data):
+ return User.objects.create(
+ username=validated_data['username'],
+ email=validated_data['email']
+ is_premium_member=validated_data['profile']['is_premium_member']
+ has_support_contract=validated_data['profile']['has_support_contract']
+ )
+
+For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost).
+
+## Dealing with multiple objects
-To map the incoming data items to their corresponding object instances, the `.get_identity()` method will be called both against the incoming data, and against the serialized representation of the existing objects.
+The `Serializer` class can also handle serializing or deserializing lists of objects.
+
+#### Serializing multiple objects
+
+To serialize a queryset or list of objects instead of a single object instance, you should pass the `many=True` flag when instantiating the serializer. You can then pass a queryset or list of objects to be serialized.
+
+ queryset = Book.objects.all()
+ serializer = BookSerializer(queryset, many=True)
+ serializer.data
+ # [
+ # {'id': 0, 'title': 'The electric kool-aid acid test', 'author': 'Tom Wolfe'},
+ # {'id': 1, 'title': 'If this is a man', 'author': 'Primo Levi'},
+ # {'id': 2, 'title': 'The wind-up bird chronicle', 'author': 'Haruki Murakami'}
+ # ]
+
+#### Deserializing multiple objects
+
+The default behavior for deserializing multiple objects is to support multiple object creation, but not support multiple object updates. For more information on how to support or customize either of these cases, see the [ListSerializer](#ListSerializer) documentation below.
## Including extra context
@@ -291,30 +411,47 @@ You can provide arbitrary additional context by passing a `context` argument whe
serializer.data
# {'id': 6, 'owner': u'denvercoder9', 'created': datetime.datetime(2013, 2, 12, 09, 44, 56, 678870), 'details': 'http://example.com/accounts/6/details'}
-The context dictionary can be used within any serializer field logic, such as a custom `.to_native()` method, by accessing the `self.context` attribute.
+The context dictionary can be used within any serializer field logic, such as a custom `.to_representation()` method, by accessing the `self.context` attribute.
---
# ModelSerializer
-Often you'll want serializer classes that map closely to model definitions.
-The `ModelSerializer` class lets you automatically create a Serializer class with fields that correspond to the Model fields.
+Often you'll want serializer classes that map closely to Django model definitions.
+
+The `ModelSerializer` class provides a shortcut that lets you automatically create a `Serializer` class with fields that correspond to the Model fields.
+
+**The `ModelSerializer` class is the same as a regular `Serializer` class, except that**:
+
+* It will automatically generate a set of fields for you, based on the model.
+* It will automatically generate validators for the serializer, such as unique_together validators.
+* It includes simple default implementations of `.create()` and `.update()`.
+
+Declaring a `ModelSerializer` looks like this:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
-By default, all the model fields on the class will be mapped to corresponding serializer fields.
+By default, all the model fields on the class will be mapped to a corresponding serializer fields.
-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.
+Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Reverse relationships are not included by default unless explicitly included as described below.
----
+#### Inspecting a `ModelSerializer`
-**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.
+Serializer classes generate helpful verbose representation strings, that allow you to fully inspect the state of their fields. This is particularly useful when working with `ModelSerializers` where you want to determine what set of fields and validators are being automatically created for you.
----
+To do so, open the Django shell, using `python manage.py shell`, then import the serializer class, instantiate it, and print the object representation…
+
+ >>> from myapp.serializers import AccountSerializer
+ >>> serializer = AccountSerializer()
+ >>> print repr(serializer) # Or `print(repr(serializer))` in Python 3.x.
+ AccountSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(allow_blank=True, max_length=100, required=False)
+ owner = PrimaryKeyRelatedField(queryset=User.objects.all())
-## Specifying which fields should be included
+## Specifying which fields to include
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`.
@@ -325,6 +462,10 @@ For example:
model = Account
fields = ('id', 'account_name', 'users', 'created')
+The names in the `fields` option will normally map to model fields on the model class.
+
+Alternatively names in the `fields` options can map to properties or methods which take no arguments that exist on the model class.
+
## Specifying nested serialization
The default `ModelSerializer` uses primary keys for relationships, but you can also easily generate nested representations using the `depth` option:
@@ -337,30 +478,70 @@ The default `ModelSerializer` uses primary keys for relationships, but you can a
The `depth` option should be set to an integer value that indicates the depth of relationships that should be traversed before reverting to a flat representation.
-## Specifying which fields should be read-only
+If you want to customize the way the serialization is done (e.g. using `allow_add_remove`) you'll need to define the field yourself.
-You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the `read_only_fields` Meta option, like so:
+## Specifying fields explicitly
+
+You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
class AccountSerializer(serializers.ModelSerializer):
+ url = serializers.CharField(source='get_absolute_url', read_only=True)
+ groups = serializers.PrimaryKeyRelatedField(many=True)
+
class Meta:
model = Account
- fields = ('id', 'account_name', 'users', 'created')
- read_only_fields = ('account_name',)
-Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
+Extra fields can correspond to any property or callable on the model.
-## Specifying fields explicitly
+## Specifying read only fields
-You can add extra fields to a `ModelSerializer` or override the default fields by declaring fields on the class, just as you would for a `Serializer` class.
+You may wish to specify multiple fields as read-only. Instead of adding each field explicitly with the `read_only=True` attribute, you may use the shortcut Meta option, `read_only_fields`.
- class AccountSerializer(serializers.ModelSerializer):
- url = serializers.CharField(source='get_absolute_url', read_only=True)
- groups = serializers.PrimaryKeyRelatedField(many=True)
+This option should be a list or tuple of field names, and is declared as follows:
+ class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
+ fields = ('id', 'account_name', 'users', 'created')
+ read_only_fields = ('account_name',)
-Extra fields can correspond to any property or callable on the model.
+Model fields which have `editable=False` set, and `AutoField` fields will be set to read-only by default, and do not need to be added to the `read_only_fields` option.
+
+---
+
+**Note**: There is a special-case where a read-only field is part of a `unique_together` constraint at the model level. In this case the field is required by the serializer class in order to validate the constraint, but should also not be editable by the user.
+
+The right way to deal with this is to specify the field explicitly on the serializer, providing both the `read_only=True` and `default=…` keyword arguments.
+
+One example of this is a read-only relation to the currently authenticated `User` which is `unique_together` with another identifier. In this case you would declare the user field like so:
+
+ user = serializers.PrimaryKeyRelatedField(read_only=True, default=serializers.CurrentUserDefault())
+
+Please review the [Validators Documentation](/api-guide/validators/) for details on the [UniqueTogetherValidator](/api-guide/validators/#uniquetogethervalidator) and [CurrentUserDefault](/api-guide/validators/#currentuserdefault) classes.
+
+---
+
+
+## Additional keyword arguments
+
+There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer.
+
+This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example:
+
+ class CreateUserSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = User
+ fields = ('email', 'username', 'password')
+ extra_kwargs = {'password': {'write_only': True}}
+
+ def create(self, validated_data):
+ user = User(
+ email=validated_data['email'],
+ username=validated_data['username']
+ )
+ user.set_password(validated_data['password'])
+ user.save()
+ return user
## Relational fields
@@ -370,6 +551,89 @@ Alternative representations include serializing using hyperlinks, serializing co
For full details see the [serializer relations][relations] documentation.
+## Inheritance of the 'Meta' class
+
+The inner `Meta` class on serializers is not inherited from parent classes by default. This is the same behavior as with Django's `Model` and `ModelForm` classes. If you want the `Meta` class to inherit from a parent class you must do so explicitly. For example:
+
+ class AccountSerializer(MyBaseSerializer):
+ class Meta(MyBaseSerializer.Meta):
+ model = Account
+
+Typically we would recommend *not* using inheritance on inner Meta classes, but instead declaring all options explicitly.
+
+## Customizing field mappings
+
+The ModelSerializer class also exposes an API that you can override in order to alter how serializer fields are automatically determined when instantiating the serializer.
+
+Normally if a `ModelSerializer` does not generate the fields you need by default the you should either add them to the class explicitly, or simply use a regular `Serializer` class instead. However in some cases you may want to create a new base class that defines how the serializer fields are created for any given model.
+
+### `.serializer_field_mapping`
+
+A mapping of Django model classes to REST framework serializer classes. You can override this mapping to alter the default serializer classes that should be used for each model class.
+
+### `.serializer_related_field`
+
+This property should be the serializer field class, that is used for relational fields by default.
+
+For `ModelSerializer` this defaults to `PrimaryKeyRelatedField`.
+
+For `HyperlinkedModelSerializer` this defaults to `serializers.HyperlinkedRelatedField`.
+
+### `serializer_url_field`
+
+The serializer field class that should be used for any `url` field on the serializer.
+
+Defaults to `serializers.HyperlinkedIdentityField`
+
+### `serializer_choice_field`
+
+The serializer field class that should be used for any choice fields on the serializer.
+
+Defaults to `serializers.ChoiceField`
+
+### The field_class and field_kwargs API
+
+The following methods are called to determine the class and keyword arguments for each field that should be automatically included on the serializer. Each of these methods should return a two tuple of `(field_class, field_kwargs)`.
+
+### `.build_standard_field(self, field_name, model_field)`
+
+Called to generate a serializer field that maps to a standard model field.
+
+The default implementation returns a serializer class based on the `serializer_field_mapping` attribute.
+
+### `.build_relational_field(self, field_name, relation_info)`
+
+Called to generate a serializer field that maps to a relational model field.
+
+The default implementation returns a serializer class based on the `serializer_relational_field` attribute.
+
+The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
+
+### `.build_nested_field(self, field_name, relation_info, nested_depth)`
+
+Called to generate a serializer field that maps to a relational model field, when the `depth` option has been set.
+
+The default implementation dynamically creates a nested serializer class based on either `ModelSerializer` or `HyperlinkedModelSerializer`.
+
+The `nested_depth` will be the value of the `depth` option, minus one.
+
+The `relation_info` argument is a named tuple, that contains `model_field`, `related_model`, `to_many` and `has_through_model` properties.
+
+### `.build_property_field(self, field_name, model_class)`
+
+Called to generate a serializer field that maps to a property or zero-argument method on the model class.
+
+The default implementation returns a `ReadOnlyField` class.
+
+### `.build_url_field(self, field_name, model_class)`
+
+Called to generate a serializer field for the serializer's own `url` field. The default implementation returns a `HyperlinkedIdentityField` class.
+
+### `.build_unknown_field(self, field_name, model_class)`
+
+Called when the field name did not map to any model field or model property.
+The default implementation raises an error, although subclasses may customize this behavior.
+
---
# HyperlinkedModelSerializer
@@ -393,22 +657,23 @@ There needs to be a way of determining which views should be used for hyperlinki
By default hyperlinks are expected to correspond to a view name that matches the style `'{model_name}-detail'`, and looks up the instance by a `pk` keyword argument.
-You can change the field that is used for object lookups by setting the `lookup_field` option. The value of this option should correspond both with a kwarg in the URL conf, and with a field on the model. For example:
+You can override a URL field view name and lookup field by using either, or both of, the `view_name` and `lookup_field` options in the `extra_kwargs` setting, like so:
class AccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Account
- fields = ('url', 'account_name', 'users', 'created')
- lookup_field = 'slug'
+ fields = ('account_url', 'account_name', 'users', 'created')
+ extra_kwargs = {
+ 'url': {'view_name': 'accounts', 'lookup_field': 'account_name'}
+ 'users': {'lookup_field': 'username'}
+ }
-Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships.
-
-For more specfic requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example:
+Alternatively you can set the fields on the serializer explicitly. For example:
class AccountSerializer(serializers.HyperlinkedModelSerializer):
url = serializers.HyperlinkedIdentityField(
- view_name='account_detail',
- lookup_field='account_name'
+ view_name='accounts',
+ lookup_field='slug'
)
users = serializers.HyperlinkedRelatedField(
view_name='user-detail',
@@ -423,13 +688,282 @@ For more specfic requirements such as specifying a different lookup for each fie
---
+**Tip**: Properly matching together hyperlinked representations and your URL conf can sometimes be a bit fiddly. Printing the `repr` of a `HyperlinkedModelSerializer` instance is a particularly useful way to inspect exactly which view names and lookup fields the relationships are expected to map too.
+
+---
+
+## Changing the URL field name
+
+The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
+
+---
+
+# ListSerializer
+
+The `ListSerializer` class provides the behavior for serializing and validating multiple objects at once. You won't *typically* need to use `ListSerializer` directly, but should instead simply pass `many=True` when instantiating a serializer.
+
+When a serializer is instantiated and `many=True` is passed, a `ListSerializer` instance will be created. The serializer class then becomes a child of the parent `ListSerializer`
+
+There *are* a few use cases when you might want to customize the `ListSerializer` behavior. For example:
+
+* You want to provide particular validation of the lists, such as always ensuring that there is at least one element in a list.
+* You want to customize the create or update behavior of multiple objects.
+
+For these cases you can modify the class that is used when `many=True` is passed, by using the `list_serializer_class` option on the serializer `Meta` class.
+
+For example:
+
+ class CustomListSerializer(serializers.ListSerializer):
+ ...
+
+ class CustomSerializer(serializers.Serializer):
+ ...
+ class Meta:
+ list_serializer_class = CustomListSerializer
+
+#### Customizing multiple create
+
+The default implementation for multiple object creation is to simply call `.create()` for each item in the list. If you want to customize this behavior, you'll need to customize the `.create()` method on `ListSerializer` class that is used when `many=True` is passed.
+
+For example:
+
+ class BookListSerializer(serializers.ListSerializer):
+ def create(self, validated_data):
+ books = [Book(**item) for item in validated_data]
+ return Book.objects.bulk_create(books)
+
+ class BookSerializer(serializers.Serializer):
+ ...
+ class Meta:
+ list_serializer_class = BookListSerializer
+
+#### Customizing multiple update
+
+By default the `ListSerializer` class does not support multiple updates. This is because the behavior that should be expected for insertions and deletions is ambiguous.
+
+To support multiple updates you'll need to do so explicitly. When writing your multiple update code make sure to keep the following in mind:
+
+* How do you determine which instance should be updated for each item in the list of data?
+* How should insertions be handled? Are they invalid, or do they create new objects?
+* How should removals be handled? Do they imply object deletion, or removing a relationship? Should they be silently ignored, or are they invalid?
+* How should ordering be handled? Does changing the position of two items imply any state change or is it ignored?
+
+Here's an example of how you might choose to implement multiple updates:
+
+ class BookListSerializer(serializers.ListSerializer):
+ def update(self, instance, validated_data):
+ # Maps for id->instance and id->data item.
+ book_mapping = {book.id: book for book in instance}
+ data_mapping = {item['id']: item for item in validated_data}
+
+ # Perform creations and updates.
+ ret = []
+ for book_id, data in data_mapping.items():
+ book = book_mapping.get(book_id, None):
+ if book is None:
+ ret.append(self.child.create(data))
+ else:
+ ret.append(self.child.update(book, data))
+
+ # Perform deletions.
+ for book_id, book in book_mapping.items():
+ if book_id not in data_mapping:
+ book.delete()
+
+ return ret
+
+ class BookSerializer(serializers.Serializer):
+ ...
+ class Meta:
+ list_serializer_class = BookListSerializer
+
+It is possible that a third party package may be included alongside the 3.1 release that provides some automatic support for multiple update operations, similar to the `allow_add_remove` behavior that was present in REST framework 2.
+
+#### Customizing ListSerializer initialization
+
+When a serializer with `many=True` is instantiated, we need to determine which arguments and keyword arguments should be passed to the `.__init__()` method for both the child `Serializer` class, and for the parent `ListSerializer` class.
+
+The default implementation is to pass all arguments to both classes, except for `validators`, and any custom keyword arguments, both of which are assumed to be intended for the child serializer class.
+
+Occasionally you might need to explicitly specify how the child and parent classes should be instantiated when `many=True` is passed. You can do so by using the `many_init` class method.
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ # Instantiate the child serializer.
+ kwargs['child'] = cls()
+ # Instantiate the parent list serializer.
+ return CustomListSerializer(*args, **kwargs)
+
+---
+
+# BaseSerializer
+
+`BaseSerializer` class that can be used to easily support alternative serialization and deserialization styles.
+
+This class implements the same basic API as the `Serializer` class:
+
+* `.data` - Returns the outgoing primitive representation.
+* `.is_valid()` - Deserializes and validates incoming data.
+* `.validated_data` - Returns the validated incoming data.
+* `.errors` - Returns an errors during validation.
+* `.save()` - Persists the validated data into an object instance.
+
+There are four methods that can be overridden, depending on what functionality you want the serializer class to support:
+
+* `.to_representation()` - Override this to support serialization, for read operations.
+* `.to_internal_value()` - Override this to support deserialization, for write operations.
+* `.create()` and `.update()` - Overide either or both of these to support saving instances.
+
+Because this class provides the same interface as the `Serializer` class, you can use it with the existing generic class based views exactly as you would for a regular `Serializer` or `ModelSerializer`.
+
+The only difference you'll notice when doing so is the `BaseSerializer` classes will not generate HTML forms in the browsable API. This is because the data they return does not include all the field information that would allow each field to be rendered into a suitable HTML input.
+
+##### Read-only `BaseSerializer` classes
+
+To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model:
+
+ class HighScore(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ player_name = models.CharField(max_length=10)
+ score = models.IntegerField()
+
+It's simple to create a read-only serializer for converting `HighScore` instances into primitive data types.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+We can now use this class to serialize single `HighScore` instances:
+
+ @api_view(['GET'])
+ def high_score(request, pk):
+ instance = HighScore.objects.get(pk=pk)
+ serializer = HighScoreSerializer(instance)
+ return Response(serializer.data)
+
+Or use it to serialize multiple instances:
+
+ @api_view(['GET'])
+ def all_high_scores(request):
+ queryset = HighScore.objects.order_by('-score')
+ serializer = HighScoreSerializer(queryset, many=True)
+ return Response(serializer.data)
+
+##### Read-write `BaseSerializer` classes
+
+To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format.
+
+Once you've implemented `.to_internal_value()`, the basic validation API will be available on the serializer, and you will be able to use `.is_valid()`, `.validated_data` and `.errors`.
+
+If you want to also support `.save()` you'll need to also implement either or both of the `.create()` and `.update()` methods.
+
+Here's a complete example of our previous `HighScoreSerializer`, that's been updated to support both read and write operations.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_internal_value(self, data):
+ score = data.get('score')
+ player_name = data.get('player_name')
+
+ # Perform the data validation.
+ if not score:
+ raise ValidationError({
+ 'score': 'This field is required.'
+ })
+ if not player_name:
+ raise ValidationError({
+ 'player_name': 'This field is required.'
+ })
+ if len(player_name) > 10:
+ raise ValidationError({
+ 'player_name': 'May not be more than 10 characters.'
+ })
+
+ # Return the validated values. This will be available as
+ # the `.validated_data` property.
+ return {
+ 'score': int(score),
+ 'player_name': player_name
+ }
+
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+ def create(self, validated_data):
+ return HighScore.objects.create(**validated_data)
+
+#### Creating new base classes
+
+The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles, or for integrating with alternative storage backends.
+
+The following class is an example of a generic serializer that can handle coercing arbitrary objects into primitive representations.
+
+ class ObjectSerializer(serializers.BaseSerializer):
+ """
+ A read-only serializer that coerces arbitrary complex objects
+ into primitive representations.
+ """
+ def to_representation(self, obj):
+ for attribute_name in dir(obj):
+ attribute = getattr(obj, attribute_name)
+ if attribute_name('_'):
+ # Ignore private attributes.
+ pass
+ elif hasattr(attribute, '__call__'):
+ # Ignore methods and other callables.
+ pass
+ elif isinstance(attribute, (str, int, bool, float, type(None))):
+ # Primitive types can be passed through unmodified.
+ output[attribute_name] = attribute
+ elif isinstance(attribute, list):
+ # Recursively deal with items in lists.
+ output[attribute_name] = [
+ self.to_representation(item) for item in attribute
+ ]
+ elif isinstance(attribute, dict):
+ # Recursively deal with items in dictionaries.
+ output[attribute_name] = {
+ str(key): self.to_representation(value)
+ for key, value in attribute.items()
+ }
+ else:
+ # Force anything else to its string representation.
+ output[attribute_name] = str(attribute)
+
+---
+
# Advanced serializer usage
-You can create customized subclasses of `ModelSerializer` or `HyperlinkedModelSerializer` that use a different set of default fields.
+## Overriding serialization and deserialization behavior
+
+If you need to alter the serialization, deserialization or validation of a serializer class you can do so by overriding the `.to_representation()` or `.to_internal_value()` methods.
+
+Some reasons this might be useful include...
+
+* Adding new behavior for new serializer base classes.
+* Modifying the behavior slightly for an existing class.
+* Improving serialization performance for a frequently accessed API endpoint that returns lots of data.
-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.
+The signatures for these methods are as follows:
-## Dynamically modifiying fields
+#### `.to_representation(self, obj)`
+
+Takes the object instance that requires serialization, and should return a primitive representation. Typically this means returning a structure of built-in Python datatypes. The exact types that can be handled will depend on the render classes you have configured for your API.
+
+#### ``.to_internal_value(self, data)``
+
+Takes the unvalidated incoming data as input and should return the validated data that will be made available as `serializer.validated_data`. The return value will also be passed to the `.create()` or `.update()` methods if `.save()` is called on the serializer class.
+
+If any of the validation fails, then the method should raise a `serializers.ValidationError(errors)`. Typically the `errors` argument here will be a dictionary mapping field names to error messages.
+
+The `data` argument passed to this method will normally be the value of `request.data`, so the datatype it provides will depend on the parser classes you have configured for your API.
+
+## Dynamically modifying 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.
@@ -448,11 +982,11 @@ For example, if you wanted to be able to set which fields should be used by a se
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
-
- # Instatiate the superclass normally
+
+ # Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
-
- if fields:
+
+ if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields.keys())
@@ -472,49 +1006,39 @@ This would then allow you to do the following:
>>> 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.
+## Customizing the default fields
-For more advanced customization than simply changing the default serializer class you can override various `get_<field_type>_field` methods. Doing so will allow you to customize the arguments that each serializer field is initialized with. Each of these methods may either return a field or serializer instance, or `None`.
+REST framework 2 provided an API to allow developers to override how a `ModelSerializer` class would automatically generate the default set of fields.
-### get_pk_field
+This API included the `.get_field()`, `.get_pk_field()` and other methods.
-**Signature**: `.get_pk_field(self, model_field)`
+Because the serializers have been fundamentally redesigned with 3.0 this API no longer exists. You can still modify the fields that get created but you'll need to refer to the source code, and be aware that if the changes you make are against private bits of API then they may be subject to change.
-Returns the field instance that should be used to represent the pk field.
+A new interface for controlling this behavior is currently planned for REST framework 3.1.
-### get_nested_field
-
-**Signature**: `.get_nested_field(self, model_field, related_model, to_many)`
-
-Returns the field instance that should be used to represent a related field when `depth` is specified as being non-zero.
-
-Note that the `model_field` argument will be `None` for reverse relationships. The `related_model` argument will be the model class for the target of the field. The `to_many` argument will be a boolean indicating if this is a to-one or to-many relationship.
-
-### get_related_field
-
-**Signature**: `.get_related_field(self, model_field, related_model, to_many)`
-
-Returns the field instance that should be used to represent a related field when `depth` is not specified, or when nested representations are being used and the depth reaches zero.
-
-Note that the `model_field` argument will be `None` for reverse relationships. The `related_model` argument will be the model class for the target of the field. The `to_many` argument will be a boolean indicating if this is a to-one or to-many relationship.
+---
-### get_field
+# Third party packages
-**Signature**: `.get_field(self, model_field)`
+The following third party packages are also available.
-Returns the field instance that should be used for non-relational, non-pk fields.
+## MongoengineModelSerializer
-### Example
+The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEngineModelSerializer` serializer class that supports using MongoDB as the storage layer for Django REST framework.
-The following custom model serializer could be used as a base class for model serializers that should always exclude the pk by default.
+## GeoFeatureModelSerializer
- class NoPKModelSerializer(serializers.ModelSerializer):
- def get_pk_field(self, model_field):
- return None
+The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations.
+## HStoreSerializer
+The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature.
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion
[relations]: relations.md
+[model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/
+[encapsulation-blogpost]: http://www.dabapps.com/blog/django-models-and-encapsulation/
+[mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine
+[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis
+[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
+[django-hstore]: https://github.com/djangonauts/django-hstore
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 7b114983..5af429d1 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -1,4 +1,4 @@
-<a class="github" href="settings.py"></a>
+source: settings.py
# Settings
@@ -12,10 +12,10 @@ For example your project's `settings.py` file might include something like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.JSONRenderer',
),
'DEFAULT_PARSER_CLASSES': (
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.JSONParser',
)
}
@@ -25,10 +25,10 @@ If you need to access the values of REST framework's API settings in your projec
you should use the `api_settings` object. For example.
from rest_framework.settings import api_settings
-
+
print api_settings.DEFAULT_AUTHENTICATION_CLASSES
-The `api_settings` object will check for any user-defined settings, and otherwise fallback to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
+The `api_settings` object will check for any user-defined settings, and otherwise fall back to the default values. Any setting that uses string import paths to refer to a class will automatically import and return the referenced class, instead of the string literal.
---
@@ -51,7 +51,7 @@ Default:
#### DEFAULT_PARSER_CLASSES
-A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.DATA` property.
+A list or tuple of parser classes, that determines the default set of parsers used when accessing the `request.data` property.
Default:
@@ -74,7 +74,7 @@ Default:
#### DEFAULT_PERMISSION_CLASSES
-A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view.
+A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. Permission must be granted by every class in the list.
Default:
@@ -100,12 +100,6 @@ Default: `'rest_framework.negotiation.DefaultContentNegotiation'`
*The following settings control the behavior of the generic class based views.*
-#### DEFAULT_MODEL_SERIALIZER_CLASS
-
-A class that determines the default type of model serializer that should be used by a generic view if `model` is specified, but `serializer_class` is not provided.
-
-Default: `'rest_framework.serializers.ModelSerializer'`
-
#### DEFAULT_PAGINATION_SERIALIZER_CLASS
A class the determines the default serialization style for paginated responses.
@@ -127,8 +121,71 @@ Default: `None`
The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size.
+For example, given the following settings:
+
+ REST_FRAMEWORK = {
+ 'PAGINATE_BY': 10,
+ 'PAGINATE_BY_PARAM': 'page_size',
+ }
+
+A client would be able to modify the pagination size by using the `page_size` query parameter. For example:
+
+ GET http://example.com/api/accounts?page_size=25
+
Default: `None`
+#### MAX_PAGINATE_BY
+
+The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied.
+
+For example, given the following settings:
+
+ REST_FRAMEWORK = {
+ 'PAGINATE_BY': 10,
+ 'PAGINATE_BY_PARAM': 'page_size',
+ 'MAX_PAGINATE_BY': 100
+ }
+
+A client request like the following would return a paginated list of up to 100 items.
+
+ GET http://example.com/api/accounts?page_size=999
+
+Default: `None`
+
+### SEARCH_PARAM
+
+The name of a query parameter, which can be used to specify the search term used by `SearchFilter`.
+
+Default: `search`
+
+#### ORDERING_PARAM
+
+The name of a query parameter, which can be used to specify the ordering of results returned by `OrderingFilter`.
+
+Default: `ordering`
+
+---
+
+## Versioning settings
+
+#### DEFAULT_VERSION
+
+The value that should be used for `request.version` when no versioning information is present.
+
+Default: `None`
+
+#### ALLOWED_VERSIONS
+
+If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set.
+
+Default: `None`
+
+#### VERSION_PARAMETER
+
+The string that should used for any versioning parameters, such as in the media type or URL query parameters.
+
+Default: `'version'`
+
---
## Authentication settings
@@ -165,7 +222,7 @@ Default: `'multipart'`
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')`
+The format of any of these renderer classes may be used when constructing a test request, for example: `client.post('/users', {'username': 'jamie'}, format='json')`
Default:
@@ -230,7 +287,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### DATETIME_INPUT_FORMATS
@@ -246,7 +303,7 @@ A format string that should be used by default for rendering the output of `Date
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### DATE_INPUT_FORMATS
@@ -262,7 +319,7 @@ A format string that should be used by default for rendering the output of `Time
May be any of `None`, `'iso-8601'` or a Python [strftime format][strftime] string.
-Default: `None`
+Default: `'iso-8601'`
#### TIME_INPUT_FORMATS
@@ -274,13 +331,121 @@ Default: `['iso-8601']`
---
+## Encodings
+
+#### UNICODE_JSON
+
+When set to `True`, JSON responses will allow unicode characters in responses. For example:
+
+ {"unicode black star":"★"}
+
+When set to `False`, JSON responses will escape non-ascii characters, like so:
+
+ {"unicode black star":"\u2605"}
+
+Both styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON. The unicode style is preferred as being more user-friendly when inspecting API responses.
+
+Default: `True`
+
+#### COMPACT_JSON
+
+When set to `True`, JSON responses will return compact representations, with no spacing after `':'` and `','` characters. For example:
+
+ {"is_admin":false,"email":"jane@example"}
+
+When set to `False`, JSON responses will return slightly more verbose representations, like so:
+
+ {"is_admin": false, "email": "jane@example"}
+
+The default style is to return minified responses, in line with [Heroku's API design guidelines][heroku-minified-json].
+
+Default: `True`
+
+#### COERCE_DECIMAL_TO_STRING
+
+When returning decimal objects in API representations that do not support a native decimal type, it is normally best to return the value as a string. This avoids the loss of precision that occurs with binary floating point implementations.
+
+When set to `True`, the serializer `DecimalField` class will return strings instead of `Decimal` objects. When set to `False`, serializers will return `Decimal` objects, which the default JSON encoder will return as floats.
+
+Default: `True`
+
+---
+
+## View names and descriptions
+
+**The following settings are used to generate the view names and descriptions, as used in responses to `OPTIONS` requests, and as used in the browsable API.**
+
+#### VIEW_NAME_FUNCTION
+
+A string representing the function that should be used when generating view names.
+
+This should be a function with the following signature:
+
+ view_name(cls, suffix=None)
+
+* `cls`: The view class. Typically the name function would inspect the name of the class when generating a descriptive name, by accessing `cls.__name__`.
+* `suffix`: The optional suffix used when differentiating individual views in a viewset.
+
+Default: `'rest_framework.views.get_view_name'`
+
+#### VIEW_DESCRIPTION_FUNCTION
+
+A string representing the function that should be used when generating view descriptions.
+
+This setting can be changed to support markup styles other than the default markdown. For example, you can use it to support `rst` markup in your view docstrings being output in the browsable API.
+
+This should be a function with the following signature:
+
+ view_description(cls, html=False)
+
+* `cls`: The view class. Typically the description function would inspect the docstring of the class when generating a description, by accessing `cls.__doc__`
+* `html`: A boolean indicating if HTML output is required. `True` when used in the browsable API, and `False` when used in generating `OPTIONS` responses.
+
+Default: `'rest_framework.views.get_view_description'`
+
+---
+
## Miscellaneous settings
+#### EXCEPTION_HANDLER
+
+A string representing the function that should be used when returning a response for any given exception. If the function returns `None`, a 500 error will be raised.
+
+This setting can be changed to support error responses other than the default `{"detail": "Failure..."}` responses. For example, you can use it to provide API responses like `{"errors": [{"message": "Failure...", "code": ""} ...]}`.
+
+This should be a function with the following signature:
+
+ exception_handler(exc, context)
+
+* `exc`: The exception.
+
+Default: `'rest_framework.views.exception_handler'`
+
+#### NON_FIELD_ERRORS_KEY
+
+A string representing the key that should be used for serializer errors that do not refer to a specific field, but are instead general errors.
+
+Default: `'non_field_errors'`
+
+#### URL_FIELD_NAME
+
+A string representing the key that should be used for the URL fields generated by `HyperlinkedModelSerializer`.
+
+Default: `'url'`
+
#### FORMAT_SUFFIX_KWARG
The name of a parameter in the URL conf that may be used to provide a format suffix.
Default: `'format'`
+#### NUM_PROXIES
+
+An integer of 0 or more, that may be used to specify the number of application proxies that the API runs behind. This allows throttling to more accurately identify client IP addresses. If set to `None` then less strict IP matching will be used by the throttle classes.
+
+Default: `None`
+
[cite]: http://www.python.org/dev/peps/pep-0020/
+[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
+[heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses
[strftime]: http://docs.python.org/2/library/time.html#time.strftime
diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md
index db2e059c..d81e092c 100644
--- a/docs/api-guide/status-codes.md
+++ b/docs/api-guide/status-codes.md
@@ -1,4 +1,4 @@
-<a class="github" href="status.py"></a>
+source: status.py
# Status Codes
@@ -9,6 +9,7 @@
Using bare status codes in your responses isn't recommended. REST framework includes a set of named constants that you can use to make more code more obvious and readable.
from rest_framework import status
+ from rest_framework.response import Response
def empty_view(self):
content = {'please move along': 'nothing to see here'}
@@ -16,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc
The full set of HTTP status codes included in the `status` module is listed below.
+The module also includes a set of helper functions for testing if a status code is in a given range.
+
+ from rest_framework import status
+ from rest_framework.test import APITestCase
+
+ class ExampleTestCase(APITestCase):
+ def test_url_root(self):
+ url = reverse('index')
+ response = self.client.get(url)
+ self.assertTrue(status.is_success(response.status_code))
+
+
For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616]
and [RFC 6585][rfc6585].
@@ -38,7 +51,7 @@ This class of status code indicates that the client's request was successfully r
HTTP_205_RESET_CONTENT
HTTP_206_PARTIAL_CONTENT
-## Redirection - 3xx
+## Redirection - 3xx
This class of status code indicates that further action needs to be taken by the user agent in order to fulfill the request.
@@ -89,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s
HTTP_505_HTTP_VERSION_NOT_SUPPORTED
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
+## Helper functions
+
+The following helper functions are available for identifying the category of the response code.
+
+ is_informational() # 1xx
+ is_success() # 2xx
+ is_redirect() # 3xx
+ is_client_error() # 4xx
+ is_server_error() # 5xx
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
[rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md
index 40b07763..8a848c20 100644
--- a/docs/api-guide/testing.md
+++ b/docs/api-guide/testing.md
@@ -1,8 +1,8 @@
-<a class="github" href="test.py"></a>
+source: test.py
# Testing
-> Code without tests is broken as designed
+> Code without tests is broken as designed.
>
> &mdash; [Jacob Kaplan-Moss][cite]
@@ -14,7 +14,9 @@ Extends [Django's existing `RequestFactory` class][requestfactory].
## 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.
+The `APIRequestFactory` class supports an almost identical API to Django's standard `RequestFactory` class. This means that the standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available.
+
+ from rest_framework.test import APIRequestFactory
# Using the standard RequestFactory API to create a form POST request
factory = APIRequestFactory()
@@ -34,7 +36,7 @@ To support a wider set of request formats, or change the default format, [see th
#### 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:
+If you need to explicitly 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')
@@ -49,6 +51,8 @@ For example, using `APIRequestFactory`, you can make a form PUT request like so:
Using Django's `RequestFactory`, you'd need to explicitly encode the data yourself:
+ from django.test.client import encode_multipart, RequestFactory
+
factory = RequestFactory()
data = {'title': 'remember to email dave'}
content = encode_multipart('BoUnDaRyStRiNg', data)
@@ -61,6 +65,8 @@ When testing views directly using a request factory, it's often convenient to be
To forcibly authenticate a request, use the `force_authenticate()` method.
+ from rest_framework.tests import force_authenticate
+
factory = APIRequestFactory()
user = User.objects.get(username='olivia')
view = AccountDetail.as_view()
@@ -72,6 +78,12 @@ To forcibly authenticate a request, use the `force_authenticate()` method.
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.
+For example, when forcibly authenticating using a token, you might do something like the following:
+
+ user = User.objects.get(username='olivia')
+ request = factory.get('/accounts/django-superstars/')
+ force_authenticate(request, user=user, token=user.token)
+
---
**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.
@@ -103,7 +115,9 @@ 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:
+The `APIClient` class supports the same request interface as Django's standard `Client` class. This means the that standard `.get()`, `.post()`, `.put()`, `.patch()`, `.delete()`, `.head()` and `.options()` methods are all available. For example:
+
+ from rest_framework.test import APIClient
client = APIClient()
client.post('/notes/', {'title': 'new idea'}, format='json')
@@ -131,8 +145,11 @@ The `login` method is appropriate for testing APIs that use session authenticati
The `credentials` method can be used to set headers that will then be included on all subsequent requests by the test client.
+ from rest_framework.authtoken.models import Token
+ from rest_framework.test import APIClient
+
# Include an appropriate `Authorization:` header on all requests.
- token = Token.objects.get(username='lauren')
+ token = Token.objects.get(user__username='lauren')
client = APIClient()
client.credentials(HTTP_AUTHORIZATION='Token ' + token.key)
@@ -155,7 +172,7 @@ This can be a useful shortcut if you're testing the API but don't want to have t
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
@@ -182,7 +199,7 @@ You can use any of REST framework's test case classes as you would for the regul
from django.core.urlresolvers import reverse
from rest_framework import status
- from rest_framework.test import APITestCase
+ from rest_framework.test import APITestCase
class AccountTests(APITestCase):
def test_create_account(self):
@@ -203,12 +220,12 @@ You can use any of REST framework's test case classes as you would for the regul
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`:
+For example, it's easier to inspect `response.data`:
response = self.client.get('/users/4/')
self.assertEqual(response.data, {'id': 4, 'username': 'lauren'})
-Instead of inspecting the result of parsing `request.content`:
+Instead of inspecting the result of parsing `response.content`:
response = self.client.get('/users/4/')
self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'})
@@ -240,18 +257,18 @@ The default format used to make test requests may be set using the `TEST_REQUEST
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.
+For example, to add support for using `format='html'` 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'
+ 'rest_framework.renderers.TemplateHTMLRenderer'
)
}
[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
+[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client
[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory
[configuration]: #configuration
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index 56f32f58..3f668867 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -1,4 +1,4 @@
-<a class="github" href="throttling.py"></a>
+source: throttling.py
# Throttling
@@ -35,7 +35,7 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/day'
- }
+ }
}
The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
@@ -43,6 +43,10 @@ The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `mi
You can also set the throttling policy on a per-view or per-viewset basis,
using the `APIView` class based views.
+ from rest_framework.response import Response
+ from rest_framework.throttling import UserRateThrottle
+ from rest_framework.views import APIView
+
class ExampleView(APIView):
throttle_classes = (UserRateThrottle,)
@@ -54,18 +58,35 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
- @api_view('GET')
- @throttle_classes(UserRateThrottle)
+ @api_view(['GET'])
+ @throttle_classes([UserRateThrottle])
def example_view(request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
+## How clients are identified
+
+The `X-Forwarded-For` and `Remote-Addr` HTTP headers are used to uniquely identify client IP addresses for throttling. If the `X-Forwarded-For` header is present then it will be used, otherwise the value of the `Remote-Addr` header will be used.
+
+If you need to strictly identify unique client IP addresses, you'll need to first configure the number of application proxies that the API runs behind by setting the `NUM_PROXIES` setting. This setting should be an integer of zero or more. If set to non-zero then the client IP will be identified as being the last IP address in the `X-Forwarded-For` header, once any application proxy IP addresses have first been excluded. If set to zero, then the `Remote-Addr` header will always be used as the identifying IP address.
+
+It is important to understand that if you configure the `NUM_PROXIES` setting, then all clients behind a unique [NAT'd](http://en.wikipedia.org/wiki/Network_address_translation) gateway will be treated as a single client.
+
+Further context on how the `X-Forwarded-For` header works, and identifying a remote client IP can be [found here][identifing-clients].
+
## Setting up the cache
The throttle classes provided by REST framework use Django's cache backend. You should make sure that you've set appropriate [cache settings][cache-setting]. The default value of `LocMemCache` backend should be okay for simple setups. See Django's [cache documentation][cache-docs] for more details.
+If you need to use a cache other than `'default'`, you can do so by creating a custom throttle class and setting the `cache` attribute. For example:
+
+ class CustomAnonRateThrottle(AnonRateThrottle):
+ cache = get_cache('alternate')
+
+You'll need to remember to also set your custom throttle class in the `'DEFAULT_THROTTLE_CLASSES'` settings key, or using the `throttle_classes` view attribute.
+
---
# API Reference
@@ -126,20 +147,20 @@ For example, given the following views...
class ContactListView(APIView):
throttle_scope = 'contacts'
...
-
+
class ContactDetailView(ApiView):
throttle_scope = 'contacts'
...
- class UploadView(APIView):
+ class UploadView(APIView):
throttle_scope = 'uploads'
...
-
+
...and the following settings.
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': (
- 'rest_framework.throttling.ScopedRateThrottle'
+ 'rest_framework.throttling.ScopedRateThrottle',
),
'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day',
@@ -157,6 +178,8 @@ To create a custom throttle, override `BaseThrottle` and implement `.allow_reque
Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recommended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.allow_request()` has previously returned `False`.
+If the `.wait()` method is implemented and the request is throttled, then a `Retry-After` header will be included in the response.
+
## Example
The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests.
@@ -167,5 +190,6 @@ The following is an example of a rate throttle, that will randomly throttle 1 in
[cite]: https://dev.twitter.com/docs/error-codes-responses
[permissions]: permissions.md
+[identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster
[cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches
[cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache
diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md
new file mode 100644
index 00000000..40ad4857
--- /dev/null
+++ b/docs/api-guide/validators.md
@@ -0,0 +1,225 @@
+source: validators.py
+
+# Validators
+
+> Validators can be useful for re-using validation logic between different types of fields.
+>
+> &mdash; [Django documentation][cite]
+
+Most of the time you're dealing with validation in REST framework you'll simply be relying on the default field validation, or writing explicit validation methods on serializer or field classes.
+
+However, sometimes you'll want to place your validation logic into reusable components, so that it can easily be reused throughout your codebase. This can be achieved by using validator functions and validator classes.
+
+## Validation in REST framework
+
+Validation in Django REST framework serializers is handled a little differently to how validation works in Django's `ModelForm` class.
+
+With `ModelForm` the validation is performed partially on the form, and partially on the model instance. With REST framework the validation is performed entirely on the serializer class. This is advantageous for the following reasons:
+
+* It introduces a proper separation of concerns, making your code behavior more obvious.
+* It is easy to switch between using shortcut `ModelSerializer` classes and using explicit `Serializer` classes. Any validation behavior being used for `ModelSerializer` is simple to replicate.
+* Printing the `repr` of a serializer instance will show you exactly what validation rules it applies. There's no extra hidden validation behavior being called on the model instance.
+
+When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using a `Serializer` classes instead, then you need to define the validation rules explicitly.
+
+#### Example
+
+As an example of how REST framework uses explicit validation, we'll take a simple model class that has a field with a uniqueness constraint.
+
+ class CustomerReportRecord(models.Model):
+ time_raised = models.DateTimeField(default=timezone.now, editable=False)
+ reference = models.CharField(unique=True, max_length=20)
+ description = models.TextField()
+
+Here's a basic `ModelSerializer` that we can use for creating or updating instances of `CustomerReportRecord`:
+
+ class CustomerReportSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = CustomerReportRecord
+
+If we open up the Django shell using `manage.py shell` we can now
+
+ >>> from project.example.serializers import CustomerReportSerializer
+ >>> serializer = CustomerReportSerializer()
+ >>> print(repr(serializer))
+ CustomerReportSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ time_raised = DateTimeField(read_only=True)
+ reference = CharField(max_length=20, validators=[<UniqueValidator(queryset=CustomerReportRecord.objects.all())>])
+ description = CharField(style={'type': 'textarea'})
+
+The interesting bit here is the `reference` field. We can see that the uniqueness constraint is being explicitly enforced by a validator on the serializer field.
+
+Because of this more explicit style REST framework includes a few validator classes that are not available in core Django. These classes are detailed below.
+
+---
+
+## UniqueValidator
+
+This validator can be used to enforce the `unique=True` constraint on model fields.
+It takes a single required argument, and an optional `messages` argument:
+
+* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
+* `message` - The error message that should be used when validation fails.
+
+This validator should be applied to *serializer fields*, like so:
+
+ slug = SlugField(
+ max_length=100,
+ validators=[UniqueValidator(queryset=BlogPost.objects.all())]
+ )
+
+## UniqueTogetherValidator
+
+This validator can be used to enforce `unique_together` constraints on model instances.
+It has two required arguments, and a single optional `messages` argument:
+
+* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
+* `fields` *required* - A list or tuple of field names which should make a unique set. These must exist as fields on the serializer class.
+* `message` - The error message that should be used when validation fails.
+
+The validator should be applied to *serializer classes*, like so:
+
+ class ExampleSerializer(serializers.Serializer):
+ # ...
+ class Meta:
+ # ToDo items belong to a parent list, and have an ordering defined
+ # by the 'position' field. No two items in a given list may share
+ # the same position.
+ validators = [
+ UniqueTogetherValidator(
+ queryset=ToDoItem.objects.all(),
+ fields=('list', 'position')
+ )
+ ]
+
+---
+
+**Note**: The `UniqueTogetherValidation` class always imposes an implicit constraint that all the fields it applies to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
+
+---
+
+## UniqueForDateValidator
+
+## UniqueForMonthValidator
+
+## UniqueForYearValidator
+
+These validators can be used to enforce the `unique_for_date`, `unique_for_month` and `unique_for_year` constraints on model instances. They take the following arguments:
+
+* `queryset` *required* - This is the queryset against which uniqueness should be enforced.
+* `field` *required* - A field name against which uniqueness in the given date range will be validated. This must exist as a field on the serializer class.
+* `date_field` *required* - A field name which will be used to determine date range for the uniqueness constrain. This must exist as a field on the serializer class.
+* `message` - The error message that should be used when validation fails.
+
+The validator should be applied to *serializer classes*, like so:
+
+ class ExampleSerializer(serializers.Serializer):
+ # ...
+ class Meta:
+ # Blog posts should have a slug that is unique for the current year.
+ validators = [
+ UniqueForYearValidator(
+ queryset=BlogPostItem.objects.all(),
+ field='slug',
+ date_field='published'
+ )
+ ]
+
+The date field that is used for the validation is always required to be present on the serializer class. You can't simply rely on a model class `default=...`, because the value being used for the default wouldn't be generated until after the validation has run.
+
+There are a couple of styles you may want to use for this depending on how you want your API to behave. If you're using `ModelSerializer` you'll probably simply rely on the defaults that REST framework generates for you, but if you are using `Serializer` or simply want more explicit control, use on of the styles demonstrated below.
+
+#### Using with a writable date field.
+
+If you want the date field to be writable the only thing worth noting is that you should ensure that it is always available in the input data, either by setting a `default` argument, or by setting `required=True`.
+
+ published = serializers.DateTimeField(required=True)
+
+#### Using with a read-only date field.
+
+If you want the date field to be visible, but not editable by the user, then set `read_only=True` and additionally set a `default=...` argument.
+
+ published = serializers.DateTimeField(read_only=True, default=timezone.now)
+
+The field will not be writable to the user, but the default value will still be passed through to the `validated_data`.
+
+#### Using with a hidden date field.
+
+If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns it's default value to the `validated_data` in the serializer.
+
+ published = serializers.HiddenField(default=timezone.now)
+
+---
+
+**Note**: The `UniqueFor<Range>Validation` classes always imposes an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input.
+
+---
+
+# Advanced 'default' argument usage
+
+Validators that are applied across multiple fields in the serializer can sometimes require a field input that should not be provided by the API client, but that *is* available as input to the validator.
+
+Two patterns that you may want to use for this sort of validation include:
+
+* Using `HiddenField`. This field will be present in `validated_data` but *will not* be used in the serializer output representation.
+* Using a standard field with `read_only=True`, but that also includes a `default=…` argument. This field *will* be used in the serializer output representation, but cannot be set directly by the user.
+
+REST framework includes a couple of defaults that may be useful in this context.
+
+#### CurrentUserDefault
+
+A default class that can be used to represent the current user. In order to use this, the 'request' must have been provided as part of the context dictionary when instantiating the serializer.
+
+ owner = serializers.HiddenField(
+ default=CurrentUserDefault()
+ )
+
+#### CreateOnlyDefault
+
+A default class that can be used to *only set a default argument during create operations*. During updates the field is omitted.
+
+It takes a single argument, which is the default value or callable that should be used during create operations.
+
+ created_at = serializers.DateTimeField(
+ read_only=True,
+ default=CreateOnlyDefault(timezone.now)
+ )
+
+---
+
+# Writing custom validators
+
+You can use any of Django's existing validators, or write your own custom validators.
+
+## Function based
+
+A validator may be any callable that raises a `serializers.ValidationError` on failure.
+
+ def even_number(value):
+ if value % 2 != 0:
+ raise serializers.ValidationError('This field must be an even number.')
+
+## Class based
+
+To write a class based validator, use the `__call__` method. Class based validators are useful as they allow you to parameterize and reuse behavior.
+
+ class MultipleOf:
+ def __init__(self, base):
+ self.base = base
+
+ def __call__(self, value):
+ if value % self.base != 0
+ message = 'This field must be a multiple of %d.' % self.base
+ raise serializers.ValidationError(message)
+
+#### Using `set_context()`
+
+In some advanced cases you might want a validator to be passed the serializer field it is being used with as additional context. You can do so by declaring a `set_context` method on a class based validator.
+
+ def set_context(self, serializer_field):
+ # Determine if this is an update or a create operation.
+ # In `__call__` we can then use that information to modify the validation behavior.
+ self.is_update = serializer_field.parent.instance is not None
+
+[cite]: https://docs.djangoproject.com/en/dev/ref/validators/
diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md
new file mode 100644
index 00000000..30dfeb2c
--- /dev/null
+++ b/docs/api-guide/versioning.md
@@ -0,0 +1,205 @@
+source: versioning.py
+
+# Versioning
+
+> Versioning an interface is just a "polite" way to kill deployed clients.
+>
+> &mdash; [Roy Fielding][cite].
+
+API versioning allows you to alter behavior between different clients. REST framework provides for a number of different versioning schemes.
+
+Versioning is determined by the incoming client request, and may either be based on the request URL, or based on the request headers.
+
+There are a number of valid approaches to approaching versioning. [Non-versioned systems can also be appropriate][roy-fielding-on-versioning], particularly if you're engineering for very long-term systems with multiple clients outside of your control.
+
+## Versioning with REST framework
+
+When API versioning is enabled, the `request.version` attribute will contain a string that corresponds to the version requested in the incoming client request.
+
+By default, versioning is not enabled, and `request.version` will always return `None`.
+
+#### Varying behavior based on the version
+
+How you vary the API behavior is up to you, but one example you might typically want is to switch to a different serialization style in a newer version. For example:
+
+ def get_serializer_class(self):
+ if self.request.version == 'v1':
+ return AccountSerializerVersion1
+ return AccountSerializer
+
+#### Reversing URLs for versioned APIs
+
+The `reverse` function included by REST framework ties in with the versioning scheme. You need to make sure to include the current `request` as a keyword argument, like so.
+
+ reverse('bookings-list', request=request)
+
+The above function will apply any URL transformations appropriate to the request version. For example:
+
+* If `NamespacedVersioning` was being used, and the API version was 'v1', then the URL lookup used would be `'v1:bookings-list'`, which might resolve to a URL like `http://example.org/v1/bookings/`.
+* If `QueryParameterVersioning` was being used, and the API version was `1.0`, then the returned URL might be something like `http://example.org/bookings/?version=1.0`
+
+#### Versioned APIs and hyperlinked serializers
+
+When using hyperlinked serialization styles together with a URL based versioning scheme make sure to include the request as context to the serializer.
+
+ def get(self, request):
+ queryset = Booking.objects.all()
+ serializer = BookingsSerializer(queryset, many=True, context={'request': request})
+ return Response({'all_bookings': serializer.data})
+
+Doing so will allow any returned URLs to include the appropriate versioning.
+
+## Configuring the versioning scheme
+
+The versioning scheme is defined by the `DEFAULT_VERSIONING_CLASS` settings key.
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning'
+ }
+
+Unless it is explicitly set, the value for `DEFAULT_VERSIONING_CLASS` will be `None`. In this case the `request.version` attribute will always return `None`.
+
+You can also set the versioning scheme on an individual view. Typically you won't need to do this, as it makes more sense to have a single versioning scheme used globally. If you do need to do so, use the `versioning_class` attribute.
+
+ class ProfileList(APIView):
+ versioning_class = versioning.QueryParameterVersioning
+
+#### Other versioning settings
+
+The following settings keys are also used to control versioning:
+
+* `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`.
+* `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Defaults to `None`.
+* `VERSION_PARAMETER`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
+
+---
+
+# API Reference
+
+## AcceptHeaderVersioning
+
+This scheme requires the client to specify the version as part of the media type in the `Accept` header. The version is included as a media type parameter, that supplements the main media type.
+
+Here's an example HTTP request using the accept header versioning style.
+
+ GET /bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/json; version=1.0
+
+In the example request above `request.version` attribute would return the string `'1.0'`.
+
+Versioning based on accept headers is [generally considered][klabnik-guidelines] as [best practice][heroku-guidelines], although other styles may be suitable depending on your client requirements.
+
+#### Using accept headers with vendor media types
+
+Strictly speaking the `json` media type is not specified as [including additional parameters][json-parameters]. If you are building a well-specified public API you might consider using a [vendor media type][vendor-media-type]. To do so, configure your renderers to use a JSON based renderer with a custom media type:
+
+ class BookingsAPIRenderer(JSONRenderer):
+ media_type = 'application/vnd.megacorp.bookings+json'
+
+Your client requests would now look like this:
+
+ GET /bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/vnd.megacorp.bookings+json; version=1.0
+
+## URLParameterVersioning
+
+This scheme requires the client to specify the version as part of the URL path.
+
+ GET /v1/bookings/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+Your URL conf must include a pattern that matches the version with a `'version'` keyword argument, so that this information is available to the versioning scheme.
+
+ urlpatterns = [
+ url(
+ r'^(?P<version>{v1,v2})/bookings/$',
+ bookings_list,
+ name='bookings-list'
+ ),
+ url(
+ r'^(?P<version>{v1,v2})/bookings/(?P<pk>[0-9]+)/$',
+ bookings_detail,
+ name='bookings-detail'
+ )
+ ]
+
+## NamespaceVersioning
+
+To the client, this scheme is the same as `URLParameterVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments.
+
+ GET /v1/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+With this scheme the `request.version` attribute is determined based on the `namespace` that matches the incoming request path.
+
+In the following example we're giving a set of views two different possible URL prefixes, each under a different namespace:
+
+ # bookings/urls.py
+ urlpatterns = [
+ url(r'^$', bookings_list, name='bookings-list'),
+ url(r'^(?P<pk>[0-9]+)/$', bookings_detail, name='bookings-detail')
+ ]
+
+ # urls.py
+ urlpatterns = [
+ url(r'^v1/bookings/', include('bookings.urls', namespace='v1')),
+ url(r'^v2/bookings/', include('bookings.urls', namespace='v2'))
+ ]
+
+Both `URLParameterVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLParameterVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects.
+
+## HostNameVersioning
+
+The hostname versioning scheme requires the client to specify the requested version as part of the hostname in the URL.
+
+For example the following is an HTTP request to the `http://v1.example.com/bookings/` URL:
+
+ GET /bookings/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+
+By default this implementation expects the hostname to match this simple regular expression:
+
+ ^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$
+
+Note that the first group is enclosed in brackets, indicating that this is the matched portion of the hostname.
+
+The `HostNameVersioning` scheme can be awkward to use in debug mode as you will typically be accessing a raw IP address such as `127.0.0.1`. There are various online services which you to [access localhost with a custom subdomain][lvh] which you may find helpful in this case.
+
+Hostname based versioning can be particularly useful if you have requirements to route incoming requests to different servers based on the version, as you can configure different DNS records for different API versions.
+
+## QueryParameterVersioning
+
+This scheme is a simple style that includes the version as a query parameter in the URL. For example:
+
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+
+---
+
+# Custom versioning schemes
+
+To implement a custom versioning scheme, subclass `BaseVersioning` and override the `.determine_version` method.
+
+## Example
+
+The following example uses a custom `X-API-Version` header to determine the requested version.
+
+ class XAPIVersionScheme(versioning.BaseVersioning):
+ def determine_version(self, request, *args, **kwargs):
+ return request.META.get('HTTP_X_API_VERSION', None)
+
+If your versioning scheme is based on the request URL, you will also want to alter how versioned URLs are determined. In order to do so you should override the `.reverse()` method on the class. See the source code for examples.
+
+[cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31
+[roy-fielding-on-versioning]: http://www.infoq.com/articles/roy-fielding-on-versioning
+[klabnik-guidelines]: http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned
+[heroku-guidelines]: https://github.com/interagent/http-api-design#version-with-accepts-header
+[json-parameters]: http://tools.ietf.org/html/rfc4627#section-6
+[vendor-media-type]: http://en.wikipedia.org/wiki/Internet_media_type#Vendor_tree
+[lvh]: https://reinteractive.net/posts/199-developing-and-testing-rails-applications-with-subdomains
diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md
index 683222d1..291fe737 100644
--- a/docs/api-guide/views.md
+++ b/docs/api-guide/views.md
@@ -1,4 +1,5 @@
-<a class="github" href="decorators.py"></a> <a class="github" href="views.py"></a>
+source: decorators.py
+ views.py
# Class Based Views
@@ -26,7 +27,7 @@ For example:
class ListUsers(APIView):
"""
View to list all users in the system.
-
+
* Requires token authentication.
* Only admin users are able to access this view.
"""
@@ -54,7 +55,7 @@ The following attributes control the pluggable aspects of API views.
### .permission_classes
-### .content_negotiation_class
+### .content_negotiation_class
## API policy instantiation methods
@@ -110,7 +111,7 @@ You won't typically need to override this method.
### .finalize_response(self, request, response, \*args, **kwargs)
-Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotation.
+Ensures that any `Response` object returned from the handler method will be rendered into the correct content type, as determined by the content negotiation.
You won't typically need to override this method.
@@ -126,19 +127,26 @@ REST framework also allows you to work with regular function based views. It pr
## @api_view()
-**Signature:** `@api_view(http_method_names)`
+**Signature:** `@api_view(http_method_names=['GET'])`
-The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
+The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
from rest_framework.decorators import api_view
- @api_view(['GET'])
+ @api_view()
def hello_world(request):
return Response({"message": "Hello, world!"})
-
This view will use the default renderers, parsers, authentication classes etc specified in the [settings].
+By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behavior, specify which methods the view allows, like so:
+
+ @api_view(['GET', 'POST'])
+ def hello_world(request):
+ if request.method == 'POST':
+ return Response({"message": "Got some data!", "data": request.data})
+ return Response({"message": "Hello, world!"})
+
## 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:
@@ -168,5 +176,5 @@ Each of these decorators takes a single argument which must be a list or tuple o
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
-[settings]: api-guide/settings.md
-[throttling]: api-guide/throttling.md
+[settings]: settings.md
+[throttling]: throttling.md
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 0c68afb0..4fd7aa84 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -1,4 +1,4 @@
-<a class="github" href="viewsets.py"></a>
+source: viewsets.py
# ViewSets
@@ -19,6 +19,12 @@ Typically, rather than explicitly registering the views in a viewset in the urlc
Let's define a simple viewset that can be used to list or retrieve all the users in the system.
+ from django.contrib.auth.models import User
+ from django.shortcuts import get_object_or_404
+ from myapps.serializers import UserSerializer
+ from rest_framework import viewsets
+ from rest_framework.response import Response
+
class UserViewSet(viewsets.ViewSet):
"""
A simple ViewSet that for listing or retrieving users.
@@ -41,6 +47,9 @@ If we need to, we can bind this viewset into two separate views, like so:
Typically we wouldn't do this, but would instead register the viewset with a router, and allow the urlconf to be automatically generated.
+ from myapp.views import UserViewSet
+ from rest_framework.routers import DefaultRouter
+
router = DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = router.urls
@@ -61,7 +70,7 @@ There are two main advantages of using a `ViewSet` class over using a `View` cla
Both of these come with a trade-off. Using regular views and URL confs is more explicit and gives you more control. ViewSets are helpful if you want to get up and running quickly, or when you have a large API and you want to enforce a consistent URL configuration throughout.
-## Marking extra methods for routing
+## Marking extra actions for routing
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below:
@@ -92,14 +101,16 @@ The default routers included with REST framework will provide routes for a stand
def destroy(self, request, pk=None):
pass
-If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests.
+If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@detail_route` or `@list_route` decorators.
+
+The `@detail_route` decorator contains `pk` in its URL pattern and is intended for methods which require a single instance. The `@list_route` decorator is intended for methods which operate on a list of objects.
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 import viewsets
+ from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer
@@ -110,10 +121,10 @@ For example:
queryset = User.objects.all()
serializer_class = UserSerializer
- @action()
+ @detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
- serializer = PasswordSerializer(data=request.DATA)
+ serializer = PasswordSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.data['password'])
user.save()
@@ -122,17 +133,27 @@ For example:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
-The `@action` and `@link` decorators can additionally take extra arguments that will be set for the routed view only. For example...
+ @list_route()
+ def recent_users(self, request):
+ recent_users = User.objects.all().order('-last_login')
+ page = self.paginate_queryset(recent_users)
+ serializer = self.get_pagination_serializer(page)
+ return Response(serializer.data)
+
+The decorators can additionally take extra arguments that will be set for the routed view only. For example...
- @action(permission_classes=[IsAdminOrIsSelf])
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
-The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
+These decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
- @action(methods=['POST', 'DELETE'])
+ @detail_route(methods=['post', 'delete'])
def unset_password(self, request, pk=None):
...
+
+The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
+
---
# API Reference
@@ -180,6 +201,8 @@ Note that you can use any of the standard attributes or method overrides provide
def get_queryset(self):
return self.request.user.accounts.all()
+Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you will have to specify the `base_name` kwarg as part of your [router registration][routers].
+
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.
## ReadOnlyModelViewSet
@@ -212,7 +235,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
"""
- A viewset that provides `retrieve`, `update`, and `list` actions.
+ A viewset that provides `retrieve`, `create`, and `list` actions.
To use it, override the class and set the `.queryset` and
`.serializer_class` attributes.
@@ -222,3 +245,4 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
[cite]: http://guides.rubyonrails.org/routing.html
+[routers]: routers.md
diff --git a/docs/img/autocomplete.png b/docs/img/autocomplete.png
deleted file mode 100644
index 29075b25..00000000
--- a/docs/img/autocomplete.png
+++ /dev/null
Binary files differ
diff --git a/docs/img/cursor-pagination.png b/docs/img/cursor-pagination.png
new file mode 100644
index 00000000..1c9c99b6
--- /dev/null
+++ b/docs/img/cursor-pagination.png
Binary files differ
diff --git a/docs/img/labels-and-milestones.png b/docs/img/labels-and-milestones.png
new file mode 100644
index 00000000..e7c829ad
--- /dev/null
+++ b/docs/img/labels-and-milestones.png
Binary files differ
diff --git a/docs/img/link-header-pagination.png b/docs/img/link-header-pagination.png
new file mode 100644
index 00000000..d3c556a4
--- /dev/null
+++ b/docs/img/link-header-pagination.png
Binary files differ
diff --git a/docs/img/logo.png b/docs/img/logo.png
new file mode 100644
index 00000000..73de34f4
--- /dev/null
+++ b/docs/img/logo.png
Binary files differ
diff --git a/docs/img/pages-pagination.png b/docs/img/pages-pagination.png
new file mode 100644
index 00000000..4ce1a09a
--- /dev/null
+++ b/docs/img/pages-pagination.png
Binary files differ
diff --git a/docs/img/sponsors/0-eventbrite.png b/docs/img/sponsors/0-eventbrite.png
new file mode 100644
index 00000000..6c739293
--- /dev/null
+++ b/docs/img/sponsors/0-eventbrite.png
Binary files differ
diff --git a/docs/img/sponsors/1-cyan.png b/docs/img/sponsors/1-cyan.png
new file mode 100644
index 00000000..d6b55b4c
--- /dev/null
+++ b/docs/img/sponsors/1-cyan.png
Binary files differ
diff --git a/docs/img/sponsors/1-divio.png b/docs/img/sponsors/1-divio.png
new file mode 100644
index 00000000..8ced88f8
--- /dev/null
+++ b/docs/img/sponsors/1-divio.png
Binary files differ
diff --git a/docs/img/sponsors/1-kuwaitnet.png b/docs/img/sponsors/1-kuwaitnet.png
new file mode 100644
index 00000000..bb779ea7
--- /dev/null
+++ b/docs/img/sponsors/1-kuwaitnet.png
Binary files differ
diff --git a/docs/img/sponsors/1-lulu.png b/docs/img/sponsors/1-lulu.png
new file mode 100644
index 00000000..8a28bfa9
--- /dev/null
+++ b/docs/img/sponsors/1-lulu.png
Binary files differ
diff --git a/docs/img/sponsors/1-potato.png b/docs/img/sponsors/1-potato.png
new file mode 100644
index 00000000..ad38abdd
--- /dev/null
+++ b/docs/img/sponsors/1-potato.png
Binary files differ
diff --git a/docs/img/sponsors/1-purplebit.png b/docs/img/sponsors/1-purplebit.png
new file mode 100644
index 00000000..0df63bf6
--- /dev/null
+++ b/docs/img/sponsors/1-purplebit.png
Binary files differ
diff --git a/docs/img/sponsors/1-runscope.png b/docs/img/sponsors/1-runscope.png
new file mode 100644
index 00000000..d80a4b85
--- /dev/null
+++ b/docs/img/sponsors/1-runscope.png
Binary files differ
diff --git a/docs/img/sponsors/1-simple-energy.png b/docs/img/sponsors/1-simple-energy.png
new file mode 100644
index 00000000..f59f7374
--- /dev/null
+++ b/docs/img/sponsors/1-simple-energy.png
Binary files differ
diff --git a/docs/img/sponsors/1-vokal_interactive.png b/docs/img/sponsors/1-vokal_interactive.png
new file mode 100644
index 00000000..431482dc
--- /dev/null
+++ b/docs/img/sponsors/1-vokal_interactive.png
Binary files differ
diff --git a/docs/img/sponsors/1-wiredrive.png b/docs/img/sponsors/1-wiredrive.png
new file mode 100644
index 00000000..c9befefe
--- /dev/null
+++ b/docs/img/sponsors/1-wiredrive.png
Binary files differ
diff --git a/docs/img/sponsors/2-byte.png b/docs/img/sponsors/2-byte.png
new file mode 100644
index 00000000..2c3777b5
--- /dev/null
+++ b/docs/img/sponsors/2-byte.png
Binary files differ
diff --git a/docs/img/sponsors/2-compile.png b/docs/img/sponsors/2-compile.png
new file mode 100644
index 00000000..858aa09d
--- /dev/null
+++ b/docs/img/sponsors/2-compile.png
Binary files differ
diff --git a/docs/img/sponsors/2-crate.png b/docs/img/sponsors/2-crate.png
new file mode 100644
index 00000000..6ef6b5da
--- /dev/null
+++ b/docs/img/sponsors/2-crate.png
Binary files differ
diff --git a/docs/img/sponsors/2-cryptico.png b/docs/img/sponsors/2-cryptico.png
new file mode 100644
index 00000000..2d86afe8
--- /dev/null
+++ b/docs/img/sponsors/2-cryptico.png
Binary files differ
diff --git a/docs/img/sponsors/2-django.png b/docs/img/sponsors/2-django.png
new file mode 100644
index 00000000..c89e19cb
--- /dev/null
+++ b/docs/img/sponsors/2-django.png
Binary files differ
diff --git a/docs/img/sponsors/2-heroku.png b/docs/img/sponsors/2-heroku.png
new file mode 100644
index 00000000..22447659
--- /dev/null
+++ b/docs/img/sponsors/2-heroku.png
Binary files differ
diff --git a/docs/img/sponsors/2-hipflask.png b/docs/img/sponsors/2-hipflask.png
new file mode 100644
index 00000000..c74735c3
--- /dev/null
+++ b/docs/img/sponsors/2-hipflask.png
Binary files differ
diff --git a/docs/img/sponsors/2-hipo.png b/docs/img/sponsors/2-hipo.png
new file mode 100644
index 00000000..2b854c6d
--- /dev/null
+++ b/docs/img/sponsors/2-hipo.png
Binary files differ
diff --git a/docs/img/sponsors/2-koordinates.png b/docs/img/sponsors/2-koordinates.png
new file mode 100644
index 00000000..f38601b3
--- /dev/null
+++ b/docs/img/sponsors/2-koordinates.png
Binary files differ
diff --git a/docs/img/sponsors/2-laterpay.png b/docs/img/sponsors/2-laterpay.png
new file mode 100644
index 00000000..75eb97d3
--- /dev/null
+++ b/docs/img/sponsors/2-laterpay.png
Binary files differ
diff --git a/docs/img/sponsors/2-lightning_kite.png b/docs/img/sponsors/2-lightning_kite.png
new file mode 100644
index 00000000..ffdced04
--- /dev/null
+++ b/docs/img/sponsors/2-lightning_kite.png
Binary files differ
diff --git a/docs/img/sponsors/2-mirus_research.png b/docs/img/sponsors/2-mirus_research.png
new file mode 100644
index 00000000..b1544070
--- /dev/null
+++ b/docs/img/sponsors/2-mirus_research.png
Binary files differ
diff --git a/docs/img/sponsors/2-nexthub.png b/docs/img/sponsors/2-nexthub.png
new file mode 100644
index 00000000..9bf76e0b
--- /dev/null
+++ b/docs/img/sponsors/2-nexthub.png
Binary files differ
diff --git a/docs/img/sponsors/2-opbeat.png b/docs/img/sponsors/2-opbeat.png
new file mode 100644
index 00000000..c71a5241
--- /dev/null
+++ b/docs/img/sponsors/2-opbeat.png
Binary files differ
diff --git a/docs/img/sponsors/2-prorenata.png b/docs/img/sponsors/2-prorenata.png
new file mode 100644
index 00000000..f5e8bb76
--- /dev/null
+++ b/docs/img/sponsors/2-prorenata.png
Binary files differ
diff --git a/docs/img/sponsors/2-pulsecode.png b/docs/img/sponsors/2-pulsecode.png
new file mode 100644
index 00000000..49f9532c
--- /dev/null
+++ b/docs/img/sponsors/2-pulsecode.png
Binary files differ
diff --git a/docs/img/sponsors/2-rapasso.png b/docs/img/sponsors/2-rapasso.png
new file mode 100644
index 00000000..618e294b
--- /dev/null
+++ b/docs/img/sponsors/2-rapasso.png
Binary files differ
diff --git a/docs/img/sponsors/2-rheinwerk_verlag.png b/docs/img/sponsors/2-rheinwerk_verlag.png
new file mode 100644
index 00000000..ad454e17
--- /dev/null
+++ b/docs/img/sponsors/2-rheinwerk_verlag.png
Binary files differ
diff --git a/docs/img/sponsors/2-schuberg_philis.png b/docs/img/sponsors/2-schuberg_philis.png
new file mode 100644
index 00000000..fd9282ee
--- /dev/null
+++ b/docs/img/sponsors/2-schuberg_philis.png
Binary files differ
diff --git a/docs/img/sponsors/2-security_compass.png b/docs/img/sponsors/2-security_compass.png
new file mode 100644
index 00000000..abd63dbe
--- /dev/null
+++ b/docs/img/sponsors/2-security_compass.png
Binary files differ
diff --git a/docs/img/sponsors/2-sga.png b/docs/img/sponsors/2-sga.png
new file mode 100644
index 00000000..2b2a3b3b
--- /dev/null
+++ b/docs/img/sponsors/2-sga.png
Binary files differ
diff --git a/docs/img/sponsors/2-singing-horse.png b/docs/img/sponsors/2-singing-horse.png
new file mode 100644
index 00000000..84142ae6
--- /dev/null
+++ b/docs/img/sponsors/2-singing-horse.png
Binary files differ
diff --git a/docs/img/sponsors/2-sirono.png b/docs/img/sponsors/2-sirono.png
new file mode 100644
index 00000000..0a243001
--- /dev/null
+++ b/docs/img/sponsors/2-sirono.png
Binary files differ
diff --git a/docs/img/sponsors/2-vinta.png b/docs/img/sponsors/2-vinta.png
new file mode 100644
index 00000000..4f4d75bc
--- /dev/null
+++ b/docs/img/sponsors/2-vinta.png
Binary files differ
diff --git a/docs/img/sponsors/2-wusawork.png b/docs/img/sponsors/2-wusawork.png
new file mode 100644
index 00000000..5834729b
--- /dev/null
+++ b/docs/img/sponsors/2-wusawork.png
Binary files differ
diff --git a/docs/img/sponsors/3-aba.png b/docs/img/sponsors/3-aba.png
new file mode 100644
index 00000000..cefa3dd6
--- /dev/null
+++ b/docs/img/sponsors/3-aba.png
Binary files differ
diff --git a/docs/img/sponsors/3-aditium.png b/docs/img/sponsors/3-aditium.png
new file mode 100644
index 00000000..0952b08c
--- /dev/null
+++ b/docs/img/sponsors/3-aditium.png
Binary files differ
diff --git a/docs/img/sponsors/3-alwaysdata.png b/docs/img/sponsors/3-alwaysdata.png
new file mode 100644
index 00000000..4095774b
--- /dev/null
+++ b/docs/img/sponsors/3-alwaysdata.png
Binary files differ
diff --git a/docs/img/sponsors/3-ax_semantics.png b/docs/img/sponsors/3-ax_semantics.png
new file mode 100644
index 00000000..c072e028
--- /dev/null
+++ b/docs/img/sponsors/3-ax_semantics.png
Binary files differ
diff --git a/docs/img/sponsors/3-beefarm.png b/docs/img/sponsors/3-beefarm.png
new file mode 100644
index 00000000..3348df42
--- /dev/null
+++ b/docs/img/sponsors/3-beefarm.png
Binary files differ
diff --git a/docs/img/sponsors/3-blimp.png b/docs/img/sponsors/3-blimp.png
new file mode 100644
index 00000000..494bf792
--- /dev/null
+++ b/docs/img/sponsors/3-blimp.png
Binary files differ
diff --git a/docs/img/sponsors/3-brightloop.png b/docs/img/sponsors/3-brightloop.png
new file mode 100644
index 00000000..8d5e85a6
--- /dev/null
+++ b/docs/img/sponsors/3-brightloop.png
Binary files differ
diff --git a/docs/img/sponsors/3-cantemo.gif b/docs/img/sponsors/3-cantemo.gif
new file mode 100644
index 00000000..17b1e8d0
--- /dev/null
+++ b/docs/img/sponsors/3-cantemo.gif
Binary files differ
diff --git a/docs/img/sponsors/3-crosswordtracker.png b/docs/img/sponsors/3-crosswordtracker.png
new file mode 100644
index 00000000..f72362ea
--- /dev/null
+++ b/docs/img/sponsors/3-crosswordtracker.png
Binary files differ
diff --git a/docs/img/sponsors/3-fluxility.png b/docs/img/sponsors/3-fluxility.png
new file mode 100644
index 00000000..eacd7da9
--- /dev/null
+++ b/docs/img/sponsors/3-fluxility.png
Binary files differ
diff --git a/docs/img/sponsors/3-garfo.png b/docs/img/sponsors/3-garfo.png
new file mode 100644
index 00000000..a9bdea0a
--- /dev/null
+++ b/docs/img/sponsors/3-garfo.png
Binary files differ
diff --git a/docs/img/sponsors/3-gizmag.png b/docs/img/sponsors/3-gizmag.png
new file mode 100644
index 00000000..a8d41bd0
--- /dev/null
+++ b/docs/img/sponsors/3-gizmag.png
Binary files differ
diff --git a/docs/img/sponsors/3-holvi.png b/docs/img/sponsors/3-holvi.png
new file mode 100644
index 00000000..255e391e
--- /dev/null
+++ b/docs/img/sponsors/3-holvi.png
Binary files differ
diff --git a/docs/img/sponsors/3-imt_computer_services.png b/docs/img/sponsors/3-imt_computer_services.png
new file mode 100644
index 00000000..00643c97
--- /dev/null
+++ b/docs/img/sponsors/3-imt_computer_services.png
Binary files differ
diff --git a/docs/img/sponsors/3-infinite_code.png b/docs/img/sponsors/3-infinite_code.png
new file mode 100644
index 00000000..7a8fdcf1
--- /dev/null
+++ b/docs/img/sponsors/3-infinite_code.png
Binary files differ
diff --git a/docs/img/sponsors/3-ipushpull.png b/docs/img/sponsors/3-ipushpull.png
new file mode 100644
index 00000000..e70b8bad
--- /dev/null
+++ b/docs/img/sponsors/3-ipushpull.png
Binary files differ
diff --git a/docs/img/sponsors/3-isl.png b/docs/img/sponsors/3-isl.png
new file mode 100644
index 00000000..0bf0cf7c
--- /dev/null
+++ b/docs/img/sponsors/3-isl.png
Binary files differ
diff --git a/docs/img/sponsors/3-life_the_game.png b/docs/img/sponsors/3-life_the_game.png
new file mode 100644
index 00000000..9292685e
--- /dev/null
+++ b/docs/img/sponsors/3-life_the_game.png
Binary files differ
diff --git a/docs/img/sponsors/3-makespace.png b/docs/img/sponsors/3-makespace.png
new file mode 100644
index 00000000..80b79361
--- /dev/null
+++ b/docs/img/sponsors/3-makespace.png
Binary files differ
diff --git a/docs/img/sponsors/3-nephila.png b/docs/img/sponsors/3-nephila.png
new file mode 100644
index 00000000..a905fa93
--- /dev/null
+++ b/docs/img/sponsors/3-nephila.png
Binary files differ
diff --git a/docs/img/sponsors/3-openeye.png b/docs/img/sponsors/3-openeye.png
new file mode 100644
index 00000000..573140ed
--- /dev/null
+++ b/docs/img/sponsors/3-openeye.png
Binary files differ
diff --git a/docs/img/sponsors/3-pathwright.png b/docs/img/sponsors/3-pathwright.png
new file mode 100644
index 00000000..71be3b28
--- /dev/null
+++ b/docs/img/sponsors/3-pathwright.png
Binary files differ
diff --git a/docs/img/sponsors/3-phurba.png b/docs/img/sponsors/3-phurba.png
new file mode 100644
index 00000000..657d872c
--- /dev/null
+++ b/docs/img/sponsors/3-phurba.png
Binary files differ
diff --git a/docs/img/sponsors/3-pkgfarm.png b/docs/img/sponsors/3-pkgfarm.png
new file mode 100644
index 00000000..9224cc2e
--- /dev/null
+++ b/docs/img/sponsors/3-pkgfarm.png
Binary files differ
diff --git a/docs/img/sponsors/3-providenz.png b/docs/img/sponsors/3-providenz.png
new file mode 100644
index 00000000..55d9c992
--- /dev/null
+++ b/docs/img/sponsors/3-providenz.png
Binary files differ
diff --git a/docs/img/sponsors/3-safari.png b/docs/img/sponsors/3-safari.png
new file mode 100644
index 00000000..c03e40e8
--- /dev/null
+++ b/docs/img/sponsors/3-safari.png
Binary files differ
diff --git a/docs/img/sponsors/3-shippo.png b/docs/img/sponsors/3-shippo.png
new file mode 100644
index 00000000..4f5ae133
--- /dev/null
+++ b/docs/img/sponsors/3-shippo.png
Binary files differ
diff --git a/docs/img/sponsors/3-teonite.png b/docs/img/sponsors/3-teonite.png
new file mode 100644
index 00000000..0c098478
--- /dev/null
+++ b/docs/img/sponsors/3-teonite.png
Binary files differ
diff --git a/docs/img/sponsors/3-thermondo-gmbh.png b/docs/img/sponsors/3-thermondo-gmbh.png
new file mode 100644
index 00000000..fe8691c8
--- /dev/null
+++ b/docs/img/sponsors/3-thermondo-gmbh.png
Binary files differ
diff --git a/docs/img/sponsors/3-tivix.png b/docs/img/sponsors/3-tivix.png
new file mode 100644
index 00000000..bc2616a6
--- /dev/null
+++ b/docs/img/sponsors/3-tivix.png
Binary files differ
diff --git a/docs/img/sponsors/3-trackmaven.png b/docs/img/sponsors/3-trackmaven.png
new file mode 100644
index 00000000..3880e370
--- /dev/null
+++ b/docs/img/sponsors/3-trackmaven.png
Binary files differ
diff --git a/docs/img/sponsors/3-transcode.png b/docs/img/sponsors/3-transcode.png
new file mode 100644
index 00000000..1faad69d
--- /dev/null
+++ b/docs/img/sponsors/3-transcode.png
Binary files differ
diff --git a/docs/img/sponsors/3-triggered_messaging.png b/docs/img/sponsors/3-triggered_messaging.png
new file mode 100644
index 00000000..4f8e5063
--- /dev/null
+++ b/docs/img/sponsors/3-triggered_messaging.png
Binary files differ
diff --git a/docs/img/sponsors/3-vzzual.png b/docs/img/sponsors/3-vzzual.png
new file mode 100644
index 00000000..98edce02
--- /dev/null
+++ b/docs/img/sponsors/3-vzzual.png
Binary files differ
diff --git a/docs/img/sponsors/3-wildfish.png b/docs/img/sponsors/3-wildfish.png
new file mode 100644
index 00000000..fa13ea70
--- /dev/null
+++ b/docs/img/sponsors/3-wildfish.png
Binary files differ
diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png
new file mode 100644
index 00000000..fec98cf9
--- /dev/null
+++ b/docs/img/travis-status.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index 99cd6b88..13f4d0c4 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,49 +1,62 @@
-<p class="badges">
+<p class="badges" height=20px>
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
-<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
+<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://www.django-rest-framework.org" data-count="none"></a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
-<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
+<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.svg?branch=master" class="travis-build-image">
</p>
-# Django REST framework
+---
-**Awesome web-browsable Web APIs.**
+**Note**: This is the documentation for the **version 3.1** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
+
+For more details see the [3.1 release notes][3.1-announcement].
+
+---
+
+<p>
+<h1 style="position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0,0,0,0);
+ border: 0;">Django REST Framework</h1>
+
+<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
+</p>
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
Some reasons you might want to use REST framework:
-* The [Web browseable API][sandbox] is a huge usability win for your developers.
-* [Authentication policies][authentication] including [OAuth1a][oauth1-section] and [OAuth2][oauth2-section] out of the box.
+* The [Web browsable API][sandbox] is a huge usability win for your developers.
+* [Authentication policies][authentication] including packages for [OAuth1a][oauth1-section] and [OAuth2][oauth2-section].
* [Serialization][serializers] that supports both [ORM][modelserializer-section] and [non-ORM][serializer-section] data sources.
* Customizable all the way down - just use [regular function-based views][functionview-section] if you don't need the [more][generic-views] [powerful][viewsets] [features][routers].
* [Extensive documentation][index], and [great community support][group].
+* Used and trusted by large companies such as [Mozilla][mozilla] and [Eventbrite][eventbrite].
-There is a live example API for testing purposes, [available here][sandbox].
-
-**Below**: *Screenshot from the browsable API*
+---
![Screenshot][image]
+**Above**: *Screenshot from the browsable API*
+
## Requirements
REST framework requires the following:
-* Python (2.6.5+, 2.7, 3.2, 3.3)
-* Django (1.3, 1.4, 1.5, 1.6)
+* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
+* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-beta)
The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
-* [PyYAML][yaml] (3.10+) - YAML content-type support.
-* [defusedxml][defusedxml] (0.3+) - XML content-type support.
-* [django-filter][django-filter] (0.5.4+) - Filtering support.
-* [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support.
-* [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support.
-
-**Note**: The `oauth2` Python package is badly misnamed, and actually provides OAuth 1.0a support. Also note that packages required for both OAuth 1.0a, and OAuth 2.0 are not yet Python 3 compatible.
+* [django-filter][django-filter] (0.9.2+) - Filtering support.
+* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
## Installation
@@ -61,15 +74,15 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
- 'rest_framework',
+ 'rest_framework',
)
If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
- urlpatterns = patterns('',
+ urlpatterns = [
...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
@@ -77,16 +90,11 @@ Note that the URL path can be whatever you want, but you must include `'rest_fra
Let's take a look at a quick example of using REST framework to build a simple model-backed API.
-We'll create a read-write API for accessing users and groups.
+We'll create a read-write API for accessing information on the users of our project.
Any global settings for a REST framework API are kept in a single configuration dictionary named `REST_FRAMEWORK`. Start off by adding the following to your `settings.py` module:
REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
-
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
@@ -99,34 +107,37 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
We're ready to create our API now.
Here's our project's root `urls.py` module:
- from django.conf.urls.defaults import url, patterns, include
- from django.contrib.auth.models import User, Group
- from rest_framework import viewsets, routers
+ from django.conf.urls import url, include
+ from django.contrib.auth.models import User
+ from rest_framework import routers, serializers, viewsets
+
+ # Serializers define the API representation.
+ class UserSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = User
+ fields = ('url', 'username', 'email', 'is_staff')
# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
- model = User
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
- class GroupViewSet(viewsets.ModelViewSet):
- model = Group
-
-
- # Routers provide an easy way of automatically determining the URL conf
+ # Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
- router.register(r'groups', GroupViewSet)
-
# Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ # Additionally, we include login URLs for the browsable API.
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
+
+You can now open the API in your browser at [http://127.0.0.1:8000/](http://127.0.0.1:8000/), and view your new 'users' API. If you use the login control in the top right corner you'll also be able to add, create and delete users from the system.
## Quickstart
-Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework.
+Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework.
## Tutorial
@@ -139,6 +150,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [5 - Relationships & hyperlinked APIs][tut-5]
* [6 - Viewsets & routers][tut-6]
+There is a live example API of the finished tutorial API for testing purposes, [available here][sandbox].
+
## API Guide
The API guide is your complete reference manual to all the functionality provided by REST framework.
@@ -154,16 +167,20 @@ The API guide is your complete reference manual to all the functionality provide
* [Serializers][serializers]
* [Serializer fields][fields]
* [Serializer relations][relations]
+* [Validators][validators]
* [Authentication][authentication]
* [Permissions][permissions]
* [Throttling][throttling]
* [Filtering][filtering]
* [Pagination][pagination]
+* [Versioning][versioning]
* [Content negotiation][contentnegotiation]
+* [Metadata][metadata]
* [Format suffixes][formatsuffixes]
* [Returning URLs][reverse]
* [Exceptions][exceptions]
* [Status codes][status]
+* [Testing][testing]
* [Settings][settings]
## Topics
@@ -175,31 +192,23 @@ General guides to using REST framework.
* [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
-* [2.0 Announcement][rest-framework-2-announcement]
-* [2.2 Announcement][2.2-announcement]
-* [2.3 Announcement][2.3-announcement]
+* [Third Party Resources][third-party-resources]
+* [Contributing to REST framework][contributing]
+* [Project management][project-management]
+* [3.0 Announcement][3.0-announcement]
+* [3.1 Announcement][3.1-announcement]
+* [Kickstarter Announcement][kickstarter-announcement]
* [Release Notes][release-notes]
-* [Credits][credits]
## Development
-If you want to work on REST framework itself, clone the repository, then...
-
-Build the docs:
-
- ./mkdocs.py
-
-Run the tests:
-
- ./rest_framework/runtests/runtests.py
-
-To run the tests against all supported configurations, first install [the tox testing tool][tox] globally, using `pip install tox`, then simply run `tox`:
-
- tox
+See the [Contribution guidelines][contributing] for information on how to clone
+the repository, run the test suite and contribute changes back to REST
+Framework.
## Support
-For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
+For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag.
[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options.
@@ -216,47 +225,44 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
## License
-Copyright (c) 2011-2013, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
-Redistribution and use in source and binary forms, with or without
+Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this
+Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this
-list of conditions and the following disclaimer in the documentation and/or
+Redistributions in binary form must reproduce the above copyright notice, this
+list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
-[urlobject]: https://github.com/zacharyvoase/urlobject
+[mozilla]: http://www.mozilla.org/en-US/about/
+[eventbrite]: https://www.eventbrite.co.uk/about/
[markdown]: http://pypi.python.org/pypi/Markdown/
-[yaml]: http://pypi.python.org/pypi/PyYAML
-[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
-[oauth2]: https://github.com/simplegeo/python-oauth2
-[django-oauth-plus]: https://bitbucket.org/david/django-oauth-plus/wiki/Home
-[django-oauth2-provider]: https://github.com/caffeinehit/django-oauth2-provider
+[django-guardian]: https://github.com/lukaszb/django-guardian
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[index]: .
-[oauth1-section]: api-guide/authentication.html#oauthauthentication
-[oauth2-section]: api-guide/authentication.html#oauth2authentication
-[serializer-section]: api-guide/serializers.html#serializers
-[modelserializer-section]: api-guide/serializers.html#modelserializer
-[functionview-section]: api-guide/views.html#function-based-views
+[oauth1-section]: api-guide/authentication/#django-rest-framework-oauth
+[oauth2-section]: api-guide/authentication/#django-oauth-toolkit
+[serializer-section]: api-guide/serializers#serializers
+[modelserializer-section]: api-guide/serializers#modelserializer
+[functionview-section]: api-guide/views#function-based-views
[sandbox]: http://restframework.herokuapp.com/
[quickstart]: tutorial/quickstart.md
@@ -278,33 +284,40 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md
[relations]: api-guide/relations.md
+[validators]: api-guide/validators.md
[authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md
[filtering]: api-guide/filtering.md
[pagination]: api-guide/pagination.md
+[versioning]: api-guide/versioning.md
[contentnegotiation]: api-guide/content-negotiation.md
+[metadata]: api-guide/metadata.md
[formatsuffixes]: api-guide/format-suffixes.md
[reverse]: api-guide/reverse.md
[exceptions]: api-guide/exceptions.md
[status]: api-guide/status-codes.md
+[testing]: api-guide/testing.md
[settings]: api-guide/settings.md
[documenting-your-api]: topics/documenting-your-api.md
+[internationalization]: topics/documenting-your-api.md
[ajax-csrf-cors]: topics/ajax-csrf-cors.md
[browser-enhancements]: topics/browser-enhancements.md
[browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
[contributing]: topics/contributing.md
-[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
-[2.2-announcement]: topics/2.2-announcement.md
-[2.3-announcement]: topics/2.3-announcement.md
+[project-management]: topics/project-management.md
+[third-party-resources]: topics/third-party-resources.md
+[3.0-announcement]: topics/3.0-announcement.md
+[3.1-announcement]: topics/3.1-announcement.md
+[kickstarter-announcement]: topics/kickstarter-announcement.md
[release-notes]: topics/release-notes.md
-[credits]: topics/credits.md
[tox]: http://testrun.org/tox/latest/
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
+[botbot]: https://botbot.me/freenode/restframework/
[stack-overflow]: http://stackoverflow.com/
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
[django-tag]: http://stackoverflow.com/questions/tagged/django
diff --git a/docs/requirements.txt b/docs/requirements.txt
deleted file mode 100644
index a91fb978..00000000
--- a/docs/requirements.txt
+++ /dev/null
@@ -1 +0,0 @@
-markdown>=2.1.0
diff --git a/docs/template.html b/docs/template.html
deleted file mode 100644
index 27bc1062..00000000
--- a/docs/template.html
+++ /dev/null
@@ -1,212 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <meta charset="utf-8">
- <title>{{ title }}</title>
- <link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="description" content="{{ description }}">
- <meta name="author" content="Tom Christie">
-
- <!-- Le styles -->
- <link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
- <link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
- <link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
- <link href="{{ base_url }}/css/default.css" rel="stylesheet">
-
- <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
- <!--[if lt IE 9]>
- <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
- <![endif]-->
-
- <script type="text/javascript">
-
- var _gaq = _gaq || [];
- _gaq.push(['_setAccount', 'UA-18852272-2']);
- _gaq.push(['_trackPageview']);
-
- (function() {
- var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
- })();
-
- </script>
- </head>
- <body onload="prettyPrint()" class="{{ page_id }}-page">
-
- <div class="wrapper">
-
- <div class="navbar navbar-inverse navbar-fixed-top">
- <div class="navbar-inner">
- <div class="container-fluid">
- <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
- <a class="repo-link btn btn-inverse btn-small {{ next_url_disabled }}" href="{{ next_url }}">Next <i class="icon-arrow-right icon-white"></i></a>
- <a class="repo-link btn btn-inverse btn-small {{ prev_url_disabled }}" href="{{ prev_url }}"><i class="icon-arrow-left icon-white"></i> Previous</a>
- <a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
- <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- <span class="icon-bar"></span>
- </a>
- <a class="brand" href="{{ base_url }}{{ index }}">Django REST framework</a>
- <div class="nav-collapse collapse">
- <ul class="nav">
- <li><a href="{{ base_url }}{{ index }}">Home</a></li>
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
- <ul class="dropdown-menu">
- <li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>
- <li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li>
- <li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li>
- <li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
- <li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
- <li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
- <li><a href="{{ base_url }}/tutorial/6-viewsets-and-routers{{ suffix }}">6 - Viewsets and routers</a></li>
- </ul>
- </li>
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
- <ul class="dropdown-menu">
- <li><a href="{{ base_url }}/api-guide/requests{{ suffix }}">Requests</a></li>
- <li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li>
- <li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li>
- <li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li>
- <li><a href="{{ base_url }}/api-guide/viewsets{{ suffix }}">Viewsets</a></li>
- <li><a href="{{ base_url }}/api-guide/routers{{ suffix }}">Routers</a></li>
- <li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li>
- <li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
- <li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
- <li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li>
- <li><a href="{{ base_url }}/api-guide/relations{{ suffix }}">Serializer relations</a></li>
- <li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
- <li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
- <li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
- <li><a href="{{ base_url }}/api-guide/filtering{{ suffix }}">Filtering</a></li>
- <li><a href="{{ base_url }}/api-guide/pagination{{ suffix }}">Pagination</a></li>
- <li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li>
- <li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li>
- <li><a href="{{ base_url }}/api-guide/reverse{{ suffix }}">Returning URLs</a></li>
- <li><a href="{{ base_url }}/api-guide/exceptions{{ suffix }}">Exceptions</a></li>
- <li><a href="{{ base_url }}/api-guide/status-codes{{ suffix }}">Status codes</a></li>
- <li><a href="{{ base_url }}/api-guide/settings{{ suffix }}">Settings</a></li>
- </ul>
- </li>
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
- <ul class="dropdown-menu">
- <li><a href="{{ base_url }}/topics/documenting-your-api{{ suffix }}">Documenting your API</a></li>
- <li><a href="{{ base_url }}/topics/ajax-csrf-cors{{ suffix }}">AJAX, CSRF & CORS</a></li>
- <li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
- <li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
- <li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
- <li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
- <li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
- <li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
- <li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
- <li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
- </ul>
- </li>
- </ul>
- <ul class="nav pull-right">
- <!-- TODO
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
- <ul class="dropdown-menu">
- <li><a href="#">Trunk</a></li>
- <li><a href="#">2.0.0</a></li>
- </ul>
- </li>
- -->
- </ul>
- </div><!--/.nav-collapse -->
- </div>
- </div>
- </div>
-
- <div class="body-content">
- <div class="container-fluid">
-
-<!-- Search Modal -->
-<div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
- <div class="modal-header">
- <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
- <h3 id="myModalLabel">Documentation search</h3>
- </div>
- <div class="modal-body">
- <!-- Custom google search -->
- <script>
- (function() {
- var cx = '015016005043623903336:rxraeohqk6w';
- var gcse = document.createElement('script');
- gcse.type = 'text/javascript';
- gcse.async = true;
- gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
- '//www.google.com/cse/cse.js?cx=' + cx;
- var s = document.getElementsByTagName('script')[0];
- s.parentNode.insertBefore(gcse, s);
- })();
- </script>
- <gcse:search></gcse:search>
- </div>
- <div class="modal-footer">
- <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
- </div>
-</div>
-
- <div class="row-fluid">
-
- <div class="span3">
- <!-- TODO
- <p style="margin-top: -12px">
- <a class="btn btn-mini btn-primary" style="width: 60px">&laquo; previous</a>
- <a class="btn btn-mini btn-primary" style="float: right; margin-right: 8px; width: 60px;">next &raquo;</a>
- </p>
- -->
- <div id="table-of-contents">
- <ul class="nav nav-list side-nav well sidebar-nav-fixed">
- {{ toc }}
- </ul>
- </div>
- </div>
-
- <div id="main-content" class="span9">
- {{ content }}
- </div><!--/span-->
- </div><!--/row-->
- </div><!--/.fluid-container-->
- </div><!--/.body content-->
-
- <div id="push"></div>
- </div><!--/.wrapper -->
-
- <footer class="span12">
- <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a></p>
- </footer>
-
- <!-- Le javascript
- ================================================== -->
- <!-- Placed at the end of the document so the pages load faster -->
- <script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
- <script src="{{ base_url }}/js/prettify-1.0.js"></script>
- <script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
- <script>
- //$('.side-nav').scrollspy()
- var shiftWindow = function() { scrollBy(0, -50) };
- if (location.hash) shiftWindow();
- window.addEventListener("hashchange", shiftWindow);
-
- $('.dropdown-menu').on('click touchstart', function(event) {
- event.stopPropagation();
- });
-
- // Dynamically force sidenav to no higher than browser window
- $('.side-nav').css('max-height', window.innerHeight - 130);
-
- $(function(){
- $(window).resize(function(){
- $('.side-nav').css('max-height', window.innerHeight - 130);
- });
- });
- </script>
-</body></html>
diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md
index 02cac129..e6220f42 100644
--- a/docs/topics/2.2-announcement.md
+++ b/docs/topics/2.2-announcement.md
@@ -1,4 +1,4 @@
-# REST framework 2.2 announcement
+# Django REST framework 2.2
The 2.2 release represents an important point for REST framework, with the addition of Python 3 support, and the introduction of an official deprecation policy.
@@ -42,7 +42,7 @@ The 2.2 release makes a few changes to the API, in order to make it more consist
The `ManyRelatedField()` style is being deprecated in favor of a new `RelatedField(many=True)` syntax.
-For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following:
+For example, if a user is associated with multiple questions, which we want to represent using a primary key relationship, we might use something like the following:
class UserSerializer(serializers.HyperlinkedModelSerializer):
questions = serializers.PrimaryKeyRelatedField(many=True)
@@ -58,10 +58,10 @@ The change also applies to serializers. If you have a nested serializer, you sh
class Meta:
model = Track
fields = ('name', 'duration')
-
+
class AlbumSerializer(serializer.ModelSerializer):
tracks = TrackSerializer(many=True)
-
+
class Meta:
model = Album
fields = ('album_name', 'artist', 'tracks')
@@ -87,7 +87,7 @@ For example, is a user account has an optional foreign key to a company, that yo
This is in line both with the rest of the serializer fields API, and with Django's `Form` and `ModelForm` API.
-Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data.
+Using `required` throughout the serializers API means you won't need to consider if a particular field should take `blank` or `null` arguments instead of `required`, and also means there will be more consistent behavior for how fields are treated when they are not present in the incoming data.
The `null=True` argument will continue to function, and will imply `required=False`, but will raise a `PendingDeprecationWarning`.
@@ -136,22 +136,22 @@ Now becomes:
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
-If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but it's use will raise a `PendingDeprecationWarning`.
+If you're overriding the `BasePermission` class, the old-style signature will continue to function, and will correctly handle both global and object-level permissions checks, but its use will raise a `PendingDeprecationWarning`.
Note also that the usage of the internal APIs for permission checking on the `View` class has been cleaned up slightly, and is now documented and subject to the deprecation policy in all future versions.
### More explicit hyperlink relations behavior
-When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fallback to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
+When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentityField`, the hyperlinks would previously use absolute URLs if the serializer context included a `'request'` key, and fall back to using relative URLs otherwise. This could lead to non-obvious behavior, as it might not be clear why some serializers generated absolute URLs, and others do not.
-From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but it's use will raise a `PendingDeprecationWarning`.
+From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`.
[xordoquy]: https://github.com/xordoquy
[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3
[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/
[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
-[credits]: http://django-rest-framework.org/topics/credits.html
+[credits]: http://www.django-rest-framework.org/topics/credits
[mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs
[marcgibbons]: https://github.com/marcgibbons/
diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md
index 9fdebcd9..21d9f1db 100644
--- a/docs/topics/2.3-announcement.md
+++ b/docs/topics/2.3-announcement.md
@@ -1,4 +1,4 @@
-# REST framework 2.3 announcement
+# Django REST framework 2.3
REST framework 2.3 makes it even quicker and easier to build your Web APIs.
@@ -15,7 +15,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
"""
A REST framework API for viewing and editing users and groups.
"""
- from django.conf.urls.defaults import url, patterns, include
+ from django.conf.urls.defaults import url, include
from django.contrib.auth.models import User, Group
from rest_framework import viewsets, routers
@@ -27,7 +27,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
class GroupViewSet(viewsets.ModelViewSet):
model = Group
-
+
# Routers provide an easy way of automatically determining the URL conf
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
@@ -35,11 +35,11 @@ As an example of just how simple REST framework APIs can now be, here's an API w
# Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ # Additionally, we include login URLs for the browsable API.
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
The best place to get started with ViewSets and Routers is to take a look at the [newest section in the tutorial][part-6], which demonstrates their usage.
@@ -131,7 +131,7 @@ The `get_object` and `get_paginate_by` methods no longer take an optional querys
Using an optional queryset with these methods continues to be supported, but will raise a `PendingDeprecationWarning`.
-The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropraite page size, or returns `None`, if pagination is not configured for the view.
+The `paginate_queryset` method no longer takes a `page_size` argument, or returns a four-tuple of pagination information. Instead it simply takes a queryset argument, and either returns a `page` object with an appropriate page size, or returns `None`, if pagination is not configured for the view.
Using the `page_size` argument is still supported and will trigger the old-style return type, but will raise a `PendingDeprecationWarning`.
@@ -195,23 +195,23 @@ Usage of the old-style attributes continues to be supported, but will raise a `P
2.3 introduces a `DecimalField` serializer field, which returns `Decimal` instances.
-For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implmentation.
+For most cases APIs using model fields will behave as previously, however if you are using a custom renderer, not provided by REST framework, then you may now need to add support for rendering `Decimal` instances to your renderer implementation.
-## ModelSerializers and reverse relationships
+## ModelSerializers and reverse relationships
The support for adding reverse relationships to the `fields` option on a `ModelSerializer` class means that the `get_related_field` and `get_nested_field` method signatures have now changed.
-In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For revese relationships `model_field` will be `None`.
+In the unlikely event that you're providing a custom serializer class, and implementing these methods you should note the new call signature for both methods is now `(self, model_field, related_model, to_many)`. For reverse relationships `model_field` will be `None`.
-The old-style signature will continue to function but will raise a `PendingDeprecationWarning`.
+The old-style signature will continue to function but will raise a `PendingDeprecationWarning`.
## View names and descriptions
-The mechanics of how the names and descriptions used in the browseable API are generated has been modified and cleaned up somewhat.
+The mechanics of how the names and descriptions used in the browsable API are generated has been modified and cleaned up somewhat.
-If you've been customizing this behavior, for example perhaps to use `rst` markup for the browseable API, then you'll need to take a look at the implementation to see what updates you need to make.
+If you've been customizing this behavior, for example perhaps to use `rst` markup for the browsable API, then you'll need to take a look at the implementation to see what updates you need to make.
-Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.
+Note that the relevant methods have always been private APIs, and the docstrings called them out as intended to be deprecated.
---
@@ -219,7 +219,7 @@ Note that the relevant methods have always been private APIs, and the docstrings
## More explicit style
-The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explict `queryset` and `serializer_class` attributes.
+The usage of `model` attribute in generic Views is still supported, but it's usage is generally being discouraged throughout the documentation, in favour of the setting the more explicit `queryset` and `serializer_class` attributes.
For example, the following is now the recommended style for using generic views:
@@ -227,7 +227,7 @@ For example, the following is now the recommended style for using generic views:
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
-Using an explict `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
+Using an explicit `queryset` and `serializer_class` attributes makes the functioning of the view more clear than using the shortcut `model` attribute.
It also makes the usage of the `get_queryset()` or `get_serializer_class()` methods more obvious.
@@ -246,7 +246,7 @@ It also makes the usage of the `get_queryset()` or `get_serializer_class()` meth
## Django 1.3 support
-The 2.3.x release series will be the last series to provide compatiblity with Django 1.3.
+The 2.3.x release series will be the last series to provide compatibility with Django 1.3.
## Version 2.2 API changes
diff --git a/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md
new file mode 100644
index 00000000..4ca35290
--- /dev/null
+++ b/docs/topics/2.4-announcement.md
@@ -0,0 +1,172 @@
+# Django REST framework 2.4
+
+The 2.4 release is largely an intermediate step, tying up some outstanding issues prior to the 3.x series.
+
+## Version requirements
+
+Support for Django 1.3 has been dropped.
+The lowest supported version of Django is now 1.4.2.
+
+The current plan is for REST framework to remain in lockstep with [Django's long-term support policy][lts-releases].
+
+## Django 1.7 support
+
+The optional authtoken application now includes support for *both* Django 1.7 schema migrations, *and* for old-style `south` migrations.
+
+**If you are using authtoken, and you want to continue using `south`, you must upgrade your `south` package to version 1.0.**
+
+## Deprecation of `.model` view attribute
+
+The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. Its usage results in more implicit, less obvious behavior.
+
+The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut.
+
+Doing so will mean that there are cases of API code where you'll now need to include a serializer class where you previously were just using the `.model` shortcut. However we firmly believe that it is the right trade-off to make.
+
+Removing the shortcut takes away an unnecessary layer of abstraction, and makes your codebase more explicit without any significant extra complexity. It also results in better consistency, as there's now only one way to set the serializer class and queryset attributes for the view, instead of two.
+
+The `DEFAULT_MODEL_SERIALIZER_CLASS` API setting is now also deprecated.
+
+## Updated test runner
+
+We now have a new test runner for developing against the project,, that uses the excellent [py.test](http://pytest.org) library.
+
+To use it make sure you have first installed the test requirements.
+
+ pip install -r requirements-test.txt
+
+Then run the `runtests.py` script.
+
+ ./runtests.py
+
+The new test runner also includes [flake8](https://flake8.readthedocs.org) code linting, which should help keep our coding style consistent.
+
+#### Test runner flags
+
+Run using a more concise output style.
+
+ ./runtests -q
+
+Run the tests using a more concise output style, no coverage, no flake8.
+
+ ./runtests --fast
+
+Don't run the flake8 code linting.
+
+ ./runtests --nolint
+
+Only run the flake8 code linting, don't run the tests.
+
+ ./runtests --lintonly
+
+Run the tests for a given test case.
+
+ ./runtests MyTestCase
+
+Run the tests for a given test method.
+
+ ./runtests MyTestCase.test_this_method
+
+Shorter form to run the tests for a given test method.
+
+ ./runtests test_this_method
+
+Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
+
+## Improved viewset routing
+
+The `@action` and `@link` decorators were inflexible in that they only allowed additional routes to be added against instance style URLs, not against list style URLs.
+
+The `@action` and `@link` decorators have now been moved to pending deprecation, and the `@list_route` and `@detail_route` decorators have been introduced.
+
+Here's an example of using the new decorators. Firstly we have a detail-type route named "set_password" that acts on a single instance, and takes a `pk` argument in the URL. Secondly we have a list-type route named "recent_users" that acts on a queryset, and does not take any arguments in the URL.
+
+ class UserViewSet(viewsets.ModelViewSet):
+ """
+ A viewset that provides the standard actions
+ """
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
+
+ @detail_route(methods=['post'])
+ def set_password(self, request, pk=None):
+ user = self.get_object()
+ serializer = PasswordSerializer(data=request.DATA)
+ if serializer.is_valid():
+ user.set_password(serializer.data['password'])
+ user.save()
+ return Response({'status': 'password set'})
+ else:
+ return Response(serializer.errors,
+ status=status.HTTP_400_BAD_REQUEST)
+
+ @list_route()
+ def recent_users(self, request):
+ recent_users = User.objects.all().order('-last_login')
+ page = self.paginate_queryset(recent_users)
+ serializer = self.get_pagination_serializer(page)
+ return Response(serializer.data)
+
+For more details, see the [viewsets documentation](../api-guide/viewsets.md).
+
+## Throttle behavior
+
+There's one bugfix in 2.4 that's worth calling out, as it will *invalidate existing throttle caches* when you upgrade.
+
+We've now fixed a typo on the `cache_format` attribute. Previously this was named `"throtte_%(scope)s_%(ident)s"`, it is now `"throttle_%(scope)s_%(ident)s"`.
+
+If you're concerned about the invalidation you have two options.
+
+* Manually pre-populate your cache with the fixed version.
+* Set the `cache_format` attribute on your throttle class in order to retain the previous incorrect spelling.
+
+## Other features
+
+There are also a number of other features and bugfixes as [listed in the release notes][2-4-release-notes]. In particular these include:
+
+[Customizable view name and description functions][view-name-and-description-settings] for use with the browsable API, by using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
+
+Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting.
+
+Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Throttle-Wait-Seconds` header which will be fully deprecated in 3.0.
+
+## Deprecations
+
+All API changes in 2.3 that previously raised `PendingDeprecationWarning` will now raise a `DeprecationWarning`, which is loud by default.
+
+All API changes in 2.3 that previously raised `DeprecationWarning` have now been removed entirely.
+
+Furter details on these deprecations is available in the [2.3 announcement][2-3-announcement].
+
+## Labels and milestones
+
+Although not strictly part of the 2.4 release it's also worth noting here that we've been working hard towards improving our triage process.
+
+The [labels that we use in GitHub][github-labels] have been cleaned up, and all existing tickets triaged. Any given ticket should have one and only one label, indicating its current state.
+
+We've also [started using milestones][github-milestones] in order to track tickets against particular releases.
+
+---
+
+![Labels and milestones](../img/labels-and-milestones.png)
+
+**Above**: *Overview of our current use of labels and milestones in GitHub.*
+
+---
+
+We hope both of these changes will help make the management process more clear and obvious and help keep tickets well-organised and relevant.
+
+## Next steps
+
+The next planned release will be 3.0, featuring an improved and simplified serializer implementation.
+
+Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible!
+
+[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
+[2-4-release-notes]: release-notes#240
+[view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions
+[client-ip-identification]: ../api-guide/throttling#how-clients-are-identified
+[2-3-announcement]: 2.3-announcement
+[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
+[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
+[kickstarter-sponsors]: kickstarter-announcement#sponsors
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
new file mode 100644
index 00000000..59fe779c
--- /dev/null
+++ b/docs/topics/3.0-announcement.md
@@ -0,0 +1,965 @@
+# Django REST framework 3.0
+
+The 3.0 release of Django REST framework is the result of almost four years of iteration and refinement. It comprehensively addresses some of the previous remaining design issues in serializers, fields and the generic views.
+
+**This release is incremental in nature. There *are* some breaking API changes, and upgrading *will* require you to read the release notes carefully, but the migration path should otherwise be relatively straightforward.**
+
+The difference in quality of the REST framework API and implementation should make writing, maintaining and debugging your application far easier.
+
+3.0 is the first of three releases that have been funded by our recent [Kickstarter campaign][kickstarter].
+
+As ever, a huge thank you to our many [wonderful sponsors][sponsors]. If you're looking for a Django gig, and want to work with smart community-minded folks, you should probably check out that list and see who's hiring.
+
+---
+
+## New features
+
+Notable features of this new release include:
+
+* Printable representations on serializers that allow you to inspect exactly what fields are present on the instance.
+* Simple model serializers that are vastly easier to understand and debug, and that make it easy to switch between the implicit `ModelSerializer` class and the explicit `Serializer` class.
+* A new `BaseSerializer` class, making it easier to write serializers for alternative storage backends, or to completely customize your serialization and validation logic.
+* A cleaner fields API including new classes such as `ListField` and `MultipleChoiceField`.
+* [Super simple default implementations][mixins.py] for the generic views.
+* Support for overriding how validation errors are handled by your API.
+* A metadata API that allows you to customize how `OPTIONS` requests are handled by your API.
+* A more compact JSON output with unicode style encoding turned on by default.
+* Templated based HTML form rendering for serializers. This will be finalized as public API in the upcoming 3.1 release.
+
+Significant new functionality continues to be planned for the 3.1 and 3.2 releases. These releases will correspond to the two [Kickstarter stretch goals](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) - "Feature improvements" and "Admin interface". Further 3.x releases will present simple upgrades, without the same level of fundamental API changes necessary for the 3.0 release.
+
+---
+
+#### REST framework: Under the hood.
+
+This talk from the [Django: Under the Hood](http://www.djangounderthehood.com/) event in Amsterdam, Nov 2014, gives some good background context on the design decisions behind 3.0.
+
+<iframe style="display: block; margin: 0 auto 0 auto" width="560" height="315" src="//www.youtube.com/embed/3cSsbe-tA0E" frameborder="0" allowfullscreen></iframe>
+
+---
+
+*Below is an in-depth guide to the API changes and migration notes for 3.0.*
+
+## Request objects
+
+#### The `.data` and `.query_params` properties.
+
+The usage of `request.DATA` and `request.FILES` is now pending deprecation in favor of a single `request.data` attribute that contains *all* the parsed data.
+
+Having separate attributes is reasonable for web applications that only ever parse url-encoded or multipart requests, but makes less sense for the general-purpose request parsing that REST framework supports.
+
+You may now pass all the request data to a serializer class in a single argument:
+
+ # Do this...
+ ExampleSerializer(data=request.data)
+
+Instead of passing the files argument separately:
+
+ # Don't do this...
+ ExampleSerializer(data=request.DATA, files=request.FILES)
+
+
+The usage of `request.QUERY_PARAMS` is now pending deprecation in favor of the lowercased `request.query_params`.
+
+---
+
+## Serializers
+
+#### Single-step object creation.
+
+Previously the serializers used a two-step object creation, as follows:
+
+1. Validating the data would create an object instance. This instance would be available as `serializer.object`.
+2. Calling `serializer.save()` would then save the object instance to the database.
+
+This style is in-line with how the `ModelForm` class works in Django, but is problematic for a number of reasons:
+
+* Some data, such as many-to-many relationships, cannot be added to the object instance until after it has been saved. This type of data needed to be hidden in some undocumented state on the object instance, or kept as state on the serializer instance so that it could be used when `.save()` is called.
+* Instantiating model instances directly means that you cannot use model manager classes for instance creation, e.g. `ExampleModel.objects.create(...)`. Manager classes are an excellent layer at which to enforce business logic and application-level data constraints.
+* The two step process makes it unclear where to put deserialization logic. For example, should extra attributes such as the current user get added to the instance during object creation or during object save?
+
+We now use single-step object creation, like so:
+
+1. Validating the data makes the cleaned data available as `serializer.validated_data`.
+2. Calling `serializer.save()` then saves and returns the new object instance.
+
+The resulting API changes are further detailed below.
+
+#### The `.create()` and `.update()` methods.
+
+The `.restore_object()` method is now removed, and we instead have two separate methods, `.create()` and `.update()`. These methods work slightly different to the previous `.restore_object()`.
+
+When using the `.create()` and `.update()` methods you should both create *and save* the object instance. This is in contrast to the previous `.restore_object()` behavior that would instantiate the object but not save it.
+
+These methods also replace the optional `.save_object()` method, which no longer exists.
+
+The following example from the tutorial previously used `restore_object()` to handle both creating and updating object instances.
+
+ def restore_object(self, attrs, instance=None):
+ if instance:
+ # Update existing instance
+ instance.title = attrs.get('title', instance.title)
+ instance.code = attrs.get('code', instance.code)
+ instance.linenos = attrs.get('linenos', instance.linenos)
+ instance.language = attrs.get('language', instance.language)
+ instance.style = attrs.get('style', instance.style)
+ return instance
+
+ # Create new instance
+ return Snippet(**attrs)
+
+This would now be split out into two separate methods.
+
+ def update(self, instance, validated_data):
+ instance.title = validated_data.get('title', instance.title)
+ instance.code = validated_data.get('code', instance.code)
+ instance.linenos = validated_data.get('linenos', instance.linenos)
+ instance.language = validated_data.get('language', instance.language)
+ instance.style = validated_data.get('style', instance.style)
+ instance.save()
+ return instance
+
+ def create(self, validated_data):
+ return Snippet.objects.create(**validated_data)
+
+Note that these methods should return the newly created object instance.
+
+#### Use `.validated_data` instead of `.object`.
+
+You must now use the `.validated_data` attribute if you need to inspect the data before saving, rather than using the `.object` attribute, which no longer exists.
+
+For example the following code *is no longer valid*:
+
+ if serializer.is_valid():
+ name = serializer.object.name # Inspect validated field data.
+ logging.info('Creating ticket "%s"' % name)
+ serializer.object.user = request.user # Include the user when saving.
+ serializer.save()
+
+Instead of using `.object` to inspect a partially constructed instance, you would now use `.validated_data` to inspect the cleaned incoming values. Also you can't set extra attributes on the instance directly, but instead pass them to the `.save()` method as keyword arguments.
+
+The corresponding code would now look like this:
+
+ if serializer.is_valid():
+ name = serializer.validated_data['name'] # Inspect validated field data.
+ logging.info('Creating ticket "%s"' % name)
+ serializer.save(user=request.user) # Include the user when saving.
+
+#### Using `.is_valid(raise_exception=True)`
+
+The `.is_valid()` method now takes an optional boolean flag, `raise_exception`.
+
+Calling `.is_valid(raise_exception=True)` will cause a `ValidationError` to be raised if the serializer data contains validation errors. This error will be handled by REST framework's default exception handler, allowing you to remove error response handling from your view code.
+
+The handling and formatting of error responses may be altered globally by using the `EXCEPTION_HANDLER` settings key.
+
+This change also means it's now possible to alter the style of error responses used by the built-in generic views, without having to include mixin classes or other overrides.
+
+#### Using `serializers.ValidationError`.
+
+Previously `serializers.ValidationError` error was simply a synonym for `django.core.exceptions.ValidationError`. This has now been altered so that it inherits from the standard `APIException` base class.
+
+The reason behind this is that Django's `ValidationError` class is intended for use with HTML forms and its API makes using it slightly awkward with nested validation errors that can occur in serializers.
+
+For most users this change shouldn't require any updates to your codebase, but it is worth ensuring that whenever raising validation errors you should prefer using the `serializers.ValidationError` exception class, and not Django's built-in exception.
+
+We strongly recommend that you use the namespaced import style of `import serializers` and not `from serializers import ValidationError` in order to avoid any potential confusion.
+
+#### Change to `validate_<field_name>`.
+
+The `validate_<field_name>` method hooks that can be attached to serializer classes change their signature slightly and return type. Previously these would take a dictionary of all incoming data, and a key representing the field name, and would return a dictionary including the validated data for that field:
+
+ def validate_score(self, attrs, source):
+ if attrs['score'] % 10 != 0:
+ raise serializers.ValidationError('This field should be a multiple of ten.')
+ return attrs
+
+This is now simplified slightly, and the method hooks simply take the value to be validated, and return the validated value.
+
+ def validate_score(self, value):
+ if value % 10 != 0:
+ raise serializers.ValidationError('This field should be a multiple of ten.')
+ return value
+
+Any ad-hoc validation that applies to more than one field should go in the `.validate(self, attrs)` method as usual.
+
+Because `.validate_<field_name>` would previously accept the complete dictionary of attributes, it could be used to validate a field depending on the input in another field. Now if you need to do this you should use `.validate()` instead.
+
+You can either return `non_field_errors` from the validate method by raising a simple `ValidationError`
+
+ def validate(self, attrs):
+ # serializer.errors == {'non_field_errors': ['A non field error']}
+ raise serializers.ValidationError('A non field error')
+
+Alternatively if you want the errors to be against a specific field, use a dictionary of when instantiating the `ValidationError`, like so:
+
+ def validate(self, attrs):
+ # serializer.errors == {'my_field': ['A field error']}
+ raise serializers.ValidationError({'my_field': 'A field error'})
+
+This ensures you can still write validation that compares all the input fields, but that marks the error against a particular field.
+
+#### Removal of `transform_<field_name>`.
+
+The under-used `transform_<field_name>` on serializer classes is no longer provided. Instead you should just override `to_representation()` if you need to apply any modifications to the representation style.
+
+For example:
+
+ def to_representation(self, instance):
+ ret = super(UserSerializer, self).to_representation(instance)
+ ret['username'] = ret['username'].lower()
+ return ret
+
+Dropping the extra point of API means there's now only one right way to do things. This helps with repetition and reinforcement of the core API, rather than having multiple differing approaches.
+
+If you absolutely need to preserve `transform_<field_name>` behavior, for example, in order to provide a simpler 2.x to 3.0 upgrade, you can use a mixin, or serializer base class that add the behavior back in. For example:
+
+ class BaseModelSerializer(ModelSerializer):
+ """
+ A custom ModelSerializer class that preserves 2.x style `transform_<field_name>` behavior.
+ """
+ def to_representation(self, instance):
+ ret = super(BaseModelSerializer, self).to_representation(instance)
+ for key, value in ret.items():
+ method = getattr(self, 'transform_' + key, None)
+ if method is not None:
+ ret[key] = method(value)
+ return ret
+
+#### Differences between ModelSerializer validation and ModelForm.
+
+This change also means that we no longer use the `.full_clean()` method on model instances, but instead perform all validation explicitly on the serializer. This gives a cleaner separation, and ensures that there's no automatic validation behavior on `ModelSerializer` classes that can't also be easily replicated on regular `Serializer` classes.
+
+For the most part this change should be transparent. Field validation and uniqueness checks will still be run as normal, but the implementation is a little different.
+
+The one difference that you do need to note is that the `.clean()` method will not be called as part of serializer validation, as it would be if using a `ModelForm`. Use the serializer `.validate()` method to perform a final validation step on incoming data where required.
+
+There may be some cases where you really do need to keep validation logic in the model `.clean()` method, and cannot instead separate it into the serializer `.validate()`. You can do so by explicitly instantiating a model instance in the `.validate()` method.
+
+ def validate(self, attrs):
+ instance = ExampleModel(**attrs)
+ instance.clean()
+ return attrs
+
+Again, you really should look at properly separating the validation logic out of the model method if possible, but the above might be useful in some backwards compatibility cases, or for an easy migration path.
+
+#### Writable nested serialization.
+
+REST framework 2.x attempted to automatically support writable nested serialization, but the behavior was complex and non-obvious. Attempting to automatically handle these case is problematic:
+
+* There can be complex dependencies involved in order of saving multiple related model instances.
+* It's unclear what behavior the user should expect when related models are passed `None` data.
+* It's unclear how the user should expect to-many relationships to handle updates, creations and deletions of multiple records.
+
+Using the `depth` option on `ModelSerializer` will now create **read-only nested serializers** by default.
+
+If you try to use a writable nested serializer without writing a custom `create()` and/or `update()` method you'll see an assertion error when you attempt to save the serializer. For example:
+
+ >>> class ProfileSerializer(serializers.ModelSerializer):
+ >>> class Meta:
+ >>> model = Profile
+ >>> fields = ('address', 'phone')
+ >>>
+ >>> class UserSerializer(serializers.ModelSerializer):
+ >>> profile = ProfileSerializer()
+ >>> class Meta:
+ >>> model = User
+ >>> fields = ('username', 'email', 'profile')
+ >>>
+ >>> data = {
+ >>> 'username': 'lizzy',
+ >>> 'email': 'lizzy@example.com',
+ >>> 'profile': {'address': '123 Acacia Avenue', 'phone': '01273 100200'}
+ >>> }
+ >>>
+ >>> serializer = UserSerializer(data=data)
+ >>> serializer.save()
+ AssertionError: The `.create()` method does not support nested writable fields by default. Write an explicit `.create()` method for serializer `UserSerializer`, or set `read_only=True` on nested serializer fields.
+
+To use writable nested serialization you'll want to declare a nested field on the serializer class, and write the `create()` and/or `update()` methods explicitly.
+
+ class UserSerializer(serializers.ModelSerializer):
+ profile = ProfileSerializer()
+
+ class Meta:
+ model = User
+ fields = ('username', 'email', 'profile')
+
+ def create(self, validated_data):
+ profile_data = validated_data.pop('profile')
+ user = User.objects.create(**validated_data)
+ Profile.objects.create(user=user, **profile_data)
+ return user
+
+The single-step object creation makes this far simpler and more obvious than the previous `.restore_object()` behavior.
+
+#### Printable serializer representations.
+
+Serializer instances now support a printable representation that allows you to inspect the fields present on the instance.
+
+For instance, given the following example model:
+
+ class LocationRating(models.Model):
+ location = models.CharField(max_length=100)
+ rating = models.IntegerField()
+ created_by = models.ForeignKey(User)
+
+Let's create a simple `ModelSerializer` class corresponding to the `LocationRating` model.
+
+ class LocationRatingSerializer(serializer.ModelSerializer):
+ class Meta:
+ model = LocationRating
+
+We can now inspect the serializer representation in the Django shell, using `python manage.py shell`...
+
+ >>> serializer = LocationRatingSerializer()
+ >>> print(serializer) # Or use `print serializer` in Python 2.x
+ LocationRatingSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ location = CharField(max_length=100)
+ rating = IntegerField()
+ created_by = PrimaryKeyRelatedField(queryset=User.objects.all())
+
+#### The `extra_kwargs` option.
+
+The `write_only_fields` option on `ModelSerializer` has been moved to `PendingDeprecation` and replaced with a more generic `extra_kwargs`.
+
+ class MySerializer(serializer.ModelSerializer):
+ class Meta:
+ model = MyModel
+ fields = ('id', 'email', 'notes', 'is_admin')
+ extra_kwargs = {
+ 'is_admin': {'write_only': True}
+ }
+
+Alternatively, specify the field explicitly on the serializer class:
+
+ class MySerializer(serializer.ModelSerializer):
+ is_admin = serializers.BooleanField(write_only=True)
+
+ class Meta:
+ model = MyModel
+ fields = ('id', 'email', 'notes', 'is_admin')
+
+The `read_only_fields` option remains as a convenient shortcut for the more common case.
+
+#### Changes to `HyperlinkedModelSerializer`.
+
+The `view_name` and `lookup_field` options have been moved to `PendingDeprecation`. They are no longer required, as you can use the `extra_kwargs` argument instead:
+
+ class MySerializer(serializer.HyperlinkedModelSerializer):
+ class Meta:
+ model = MyModel
+ fields = ('url', 'email', 'notes', 'is_admin')
+ extra_kwargs = {
+ 'url': {'lookup_field': 'uuid'}
+ }
+
+Alternatively, specify the field explicitly on the serializer class:
+
+ class MySerializer(serializer.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(
+ view_name='mymodel-detail',
+ lookup_field='uuid'
+ )
+
+ class Meta:
+ model = MyModel
+ fields = ('url', 'email', 'notes', 'is_admin')
+
+#### Fields for model methods and properties.
+
+With `ModelSerializer` you can now specify field names in the `fields` option that refer to model methods or properties. For example, suppose you have the following model:
+
+ class Invitation(models.Model):
+ created = models.DateTimeField()
+ to_email = models.EmailField()
+ message = models.CharField(max_length=1000)
+
+ def expiry_date(self):
+ return self.created + datetime.timedelta(days=30)
+
+You can include `expiry_date` as a field option on a `ModelSerializer` class.
+
+ class InvitationSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Invitation
+ fields = ('to_email', 'message', 'expiry_date')
+
+These fields will be mapped to `serializers.ReadOnlyField()` instances.
+
+ >>> serializer = InvitationSerializer()
+ >>> print repr(serializer)
+ InvitationSerializer():
+ to_email = EmailField(max_length=75)
+ message = CharField(max_length=1000)
+ expiry_date = ReadOnlyField()
+
+#### The `ListSerializer` class.
+
+The `ListSerializer` class has now been added, and allows you to create base serializer classes for only accepting multiple inputs.
+
+ class MultipleUserSerializer(ListSerializer):
+ child = UserSerializer()
+
+You can also still use the `many=True` argument to serializer classes. It's worth noting that `many=True` argument transparently creates a `ListSerializer` instance, allowing the validation logic for list and non-list data to be cleanly separated in the REST framework codebase.
+
+You will typically want to *continue to use the existing `many=True` flag* rather than declaring `ListSerializer` classes explicitly, but declaring the classes explicitly can be useful if you need to write custom `create` or `update` methods for bulk updates, or provide for other custom behavior.
+
+See also the new `ListField` class, which validates input in the same way, but does not include the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on.
+
+#### The `BaseSerializer` class.
+
+REST framework now includes a simple `BaseSerializer` class that can be used to easily support alternative serialization and deserialization styles.
+
+This class implements the same basic API as the `Serializer` class:
+
+* `.data` - Returns the outgoing primitive representation.
+* `.is_valid()` - Deserializes and validates incoming data.
+* `.validated_data` - Returns the validated incoming data.
+* `.errors` - Returns an errors during validation.
+* `.save()` - Persists the validated data into an object instance.
+
+There are four methods that can be overridden, depending on what functionality you want the serializer class to support:
+
+* `.to_representation()` - Override this to support serialization, for read operations.
+* `.to_internal_value()` - Override this to support deserialization, for write operations.
+* `.create()` and `.update()` - Override either or both of these to support saving instances.
+
+Because this class provides the same interface as the `Serializer` class, you can use it with the existing generic class based views exactly as you would for a regular `Serializer` or `ModelSerializer`.
+
+The only difference you'll notice when doing so is the `BaseSerializer` classes will not generate HTML forms in the browsable API. This is because the data they return does not include all the field information that would allow each field to be rendered into a suitable HTML input.
+
+##### Read-only `BaseSerializer` classes.
+
+To implement a read-only serializer using the `BaseSerializer` class, we just need to override the `.to_representation()` method. Let's take a look at an example using a simple Django model:
+
+ class HighScore(models.Model):
+ created = models.DateTimeField(auto_now_add=True)
+ player_name = models.CharField(max_length=10)
+ score = models.IntegerField()
+
+It's simple to create a read-only serializer for converting `HighScore` instances into primitive data types.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+We can now use this class to serialize single `HighScore` instances:
+
+ @api_view(['GET'])
+ def high_score(request, pk):
+ instance = HighScore.objects.get(pk=pk)
+ serializer = HighScoreSerializer(instance)
+ return Response(serializer.data)
+
+Or use it to serialize multiple instances:
+
+ @api_view(['GET'])
+ def all_high_scores(request):
+ queryset = HighScore.objects.order_by('-score')
+ serializer = HighScoreSerializer(queryset, many=True)
+ return Response(serializer.data)
+
+##### Read-write `BaseSerializer` classes.
+
+To create a read-write serializer we first need to implement a `.to_internal_value()` method. This method returns the validated values that will be used to construct the object instance, and may raise a `ValidationError` if the supplied data is in an incorrect format.
+
+Once you've implemented `.to_internal_value()`, the basic validation API will be available on the serializer, and you will be able to use `.is_valid()`, `.validated_data` and `.errors`.
+
+If you want to also support `.save()` you'll need to also implement either or both of the `.create()` and `.update()` methods.
+
+Here's a complete example of our previous `HighScoreSerializer`, that's been updated to support both read and write operations.
+
+ class HighScoreSerializer(serializers.BaseSerializer):
+ def to_internal_value(self, data):
+ score = data.get('score')
+ player_name = data.get('player_name')
+
+ # Perform the data validation.
+ if not score:
+ raise ValidationError({
+ 'score': 'This field is required.'
+ })
+ if not player_name:
+ raise ValidationError({
+ 'player_name': 'This field is required.'
+ })
+ if len(player_name) > 10:
+ raise ValidationError({
+ 'player_name': 'May not be more than 10 characters.'
+ })
+
+ # Return the validated values. This will be available as
+ # the `.validated_data` property.
+ return {
+ 'score': int(score),
+ 'player_name': player_name
+ }
+
+ def to_representation(self, obj):
+ return {
+ 'score': obj.score,
+ 'player_name': obj.player_name
+ }
+
+ def create(self, validated_data):
+ return HighScore.objects.create(**validated_data)
+
+#### Creating new generic serializers with `BaseSerializer`.
+
+The `BaseSerializer` class is also useful if you want to implement new generic serializer classes for dealing with particular serialization styles, or for integrating with alternative storage backends.
+
+The following class is an example of a generic serializer that can handle coercing arbitrary objects into primitive representations.
+
+ class ObjectSerializer(serializers.BaseSerializer):
+ """
+ A read-only serializer that coerces arbitrary complex objects
+ into primitive representations.
+ """
+ def to_representation(self, obj):
+ for attribute_name in dir(obj):
+ attribute = getattr(obj, attribute_name)
+ if attribute_name('_'):
+ # Ignore private attributes.
+ pass
+ elif hasattr(attribute, '__call__'):
+ # Ignore methods and other callables.
+ pass
+ elif isinstance(attribute, (str, int, bool, float, type(None))):
+ # Primitive types can be passed through unmodified.
+ output[attribute_name] = attribute
+ elif isinstance(attribute, list):
+ # Recursively deal with items in lists.
+ output[attribute_name] = [
+ self.to_representation(item) for item in attribute
+ ]
+ elif isinstance(attribute, dict):
+ # Recursively deal with items in dictionaries.
+ output[attribute_name] = {
+ str(key): self.to_representation(value)
+ for key, value in attribute.items()
+ }
+ else:
+ # Force anything else to its string representation.
+ output[attribute_name] = str(attribute)
+
+---
+
+## Serializer fields
+
+#### The `Field` and `ReadOnly` field classes.
+
+There are some minor tweaks to the field base classes.
+
+Previously we had these two base classes:
+
+* `Field` as the base class for read-only fields. A default implementation was included for serializing data.
+* `WritableField` as the base class for read-write fields.
+
+We now use the following:
+
+* `Field` is the base class for all fields. It does not include any default implementation for either serializing or deserializing data.
+* `ReadOnlyField` is a concrete implementation for read-only fields that simply returns the attribute value without modification.
+
+#### The `required`, `allow_null`, `allow_blank` and `default` arguments.
+
+REST framework now has more explicit and clear control over validating empty values for fields.
+
+Previously the meaning of the `required=False` keyword argument was underspecified. In practice its use meant that a field could either be not included in the input, or it could be included, but be `None` or the empty string.
+
+We now have a better separation, with separate `required`, `allow_null` and `allow_blank` arguments.
+
+The following set of arguments are used to control validation of empty values:
+
+* `required=False`: The value does not need to be present in the input, and will not be passed to `.create()` or `.update()` if it is not seen.
+* `default=<value>`: The value does not need to be present in the input, and a default value will be passed to `.create()` or `.update()` if it is not seen.
+* `allow_null=True`: `None` is a valid input.
+* `allow_blank=True`: `''` is valid input. For `CharField` and subclasses only.
+
+Typically you'll want to use `required=False` if the corresponding model field has a default value, and additionally set either `allow_null=True` or `allow_blank=True` if required.
+
+The `default` argument is also available and always implies that the field is not required to be in the input. It is unnecessary to use the `required` argument when a default is specified, and doing so will result in an error.
+
+#### Coercing output types.
+
+The previous field implementations did not forcibly coerce returned values into the correct type in many cases. For example, an `IntegerField` would return a string output if the attribute value was a string. We now more strictly coerce to the correct return type, leading to more constrained and expected behavior.
+
+#### Removal of `.validate()`.
+
+The `.validate()` method is now removed from field classes. This method was in any case undocumented and not public API. You should instead simply override `to_internal_value()`.
+
+ class UppercaseCharField(serializers.CharField):
+ def to_internal_value(self, data):
+ value = super(UppercaseCharField, self).to_internal_value(data)
+ if value != value.upper():
+ raise serializers.ValidationError('The input should be uppercase only.')
+ return value
+
+Previously validation errors could be raised in either `.to_native()` or `.validate()`, making it non-obvious which should be used. Providing only a single point of API ensures more repetition and reinforcement of the core API.
+
+#### The `ListField` class.
+
+The `ListField` class has now been added. This field validates list input. It takes a `child` keyword argument which is used to specify the field used to validate each item in the list. For example:
+
+ scores = ListField(child=IntegerField(min_value=0, max_value=100))
+
+You can also use a declarative style to create new subclasses of `ListField`, like this:
+
+ class ScoresField(ListField):
+ child = IntegerField(min_value=0, max_value=100)
+
+We can now use the `ScoresField` class inside another serializer:
+
+ scores = ScoresField()
+
+See also the new `ListSerializer` class, which validates input in the same way, but also includes the serializer interfaces of `.is_valid()`, `.data`, `.save()` and so on.
+
+#### The `ChoiceField` class may now accept a flat list.
+
+The `ChoiceField` class may now accept a list of choices in addition to the existing style of using a list of pairs of `(name, display_value)`. The following is now valid:
+
+ color = ChoiceField(choices=['red', 'green', 'blue'])
+
+#### The `MultipleChoiceField` class.
+
+The `MultipleChoiceField` class has been added. This field acts like `ChoiceField`, but returns a set, which may include none, one or many of the valid choices.
+
+#### Changes to the custom field API.
+
+The `from_native(self, value)` and `to_native(self, data)` method names have been replaced with the more obviously named `to_internal_value(self, data)` and `to_representation(self, value)`.
+
+The `field_from_native()` and `field_to_native()` methods are removed. Previously you could use these methods if you wanted to customise the behaviour in a way that did not simply lookup the field value from the object. For example...
+
+ def field_to_native(self, obj, field_name):
+ """A custom read-only field that returns the class name."""
+ return obj.__class__.__name__
+
+Now if you need to access the entire object you'll instead need to override one or both of the following:
+
+* Use `get_attribute` to modify the attribute value passed to `to_representation()`.
+* Use `get_value` to modify the data value passed `to_internal_value()`.
+
+For example:
+
+ def get_attribute(self, obj):
+ # Pass the entire object through to `to_representation()`,
+ # instead of the standard attribute lookup.
+ return obj
+
+ def to_representation(self, value):
+ return value.__class__.__name__
+
+#### Explicit `queryset` required on relational fields.
+
+Previously relational fields that were explicitly declared on a serializer class could omit the queryset argument if (and only if) they were declared on a `ModelSerializer`.
+
+This code *would be valid* in `2.4.3`:
+
+ class AccountSerializer(serializers.ModelSerializer):
+ organizations = serializers.SlugRelatedField(slug_field='name')
+
+ class Meta:
+ model = Account
+
+However this code *would not be valid* in `3.0`:
+
+ # Missing `queryset`
+ class AccountSerializer(serializers.Serializer):
+ organizations = serializers.SlugRelatedField(slug_field='name')
+
+ def restore_object(self, attrs, instance=None):
+ # ...
+
+The queryset argument is now always required for writable relational fields.
+This removes some magic and makes it easier and more obvious to move between implicit `ModelSerializer` classes and explicit `Serializer` classes.
+
+ class AccountSerializer(serializers.ModelSerializer):
+ organizations = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=Organization.objects.all()
+ )
+
+ class Meta:
+ model = Account
+
+The `queryset` argument is only ever required for writable fields, and is not required or valid for fields with `read_only=True`.
+
+#### Optional argument to `SerializerMethodField`.
+
+The argument to `SerializerMethodField` is now optional, and defaults to `get_<field_name>`. For example the following is valid:
+
+ class AccountSerializer(serializers.Serializer):
+ # `method_name='get_billing_details'` by default.
+ billing_details = serializers.SerializerMethodField()
+
+ def get_billing_details(self, account):
+ return calculate_billing(account)
+
+In order to ensure a consistent code style an assertion error will be raised if you include a redundant method name argument that matches the default method name. For example, the following code *will raise an error*:
+
+ billing_details = serializers.SerializerMethodField('get_billing_details')
+
+#### Enforcing consistent `source` usage.
+
+I've see several codebases that unnecessarily include the `source` argument, setting it to the same value as the field name. This usage is redundant and confusing, making it less obvious that `source` is usually not required.
+
+The following usage will *now raise an error*:
+
+ email = serializers.EmailField(source='email')
+
+#### The `UniqueValidator` and `UniqueTogetherValidator` classes.
+
+REST framework now provides new validators that allow you to ensure field uniqueness, while still using a completely explicit `Serializer` class instead of using `ModelSerializer`.
+
+The `UniqueValidator` should be applied to a serializer field, and takes a single `queryset` argument.
+
+ from rest_framework import serializers
+ from rest_framework.validators import UniqueValidator
+
+ class OrganizationSerializer(serializers.Serializer):
+ url = serializers.HyperlinkedIdentityField(view_name='organization_detail')
+ created = serializers.DateTimeField(read_only=True)
+ name = serializers.CharField(
+ max_length=100,
+ validators=UniqueValidator(queryset=Organization.objects.all())
+ )
+
+The `UniqueTogetherValidator` should be applied to a serializer, and takes a `queryset` argument and a `fields` argument which should be a list or tuple of field names.
+
+ class RaceResultSerializer(serializers.Serializer):
+ category = serializers.ChoiceField(['5k', '10k'])
+ position = serializers.IntegerField()
+ name = serializers.CharField(max_length=100)
+
+ class Meta:
+ validators = [UniqueTogetherValidator(
+ queryset=RaceResult.objects.all(),
+ fields=('category', 'position')
+ )]
+
+#### The `UniqueForDateValidator` classes.
+
+REST framework also now includes explicit validator classes for validating the `unique_for_date`, `unique_for_month`, and `unique_for_year` model field constraints. These are used internally instead of calling into `Model.full_clean()`.
+
+These classes are documented in the [Validators](../api-guide/validators.md) section of the documentation.
+
+---
+
+## Generic views
+
+#### Simplification of view logic.
+
+The view logic for the default method handlers has been significantly simplified, due to the new serializers API.
+
+#### Changes to pre/post save hooks.
+
+The `pre_save` and `post_save` hooks no longer exist, but are replaced with `perform_create(self, serializer)` and `perform_update(self, serializer)`.
+
+These methods should save the object instance by calling `serializer.save()`, adding in any additional arguments as required. They may also perform any custom pre-save or post-save behavior.
+
+For example:
+
+ def perform_create(self, serializer):
+ # Include the owner attribute directly, rather than from request data.
+ instance = serializer.save(owner=self.request.user)
+ # Perform a custom post-save action.
+ send_email(instance.to_email, instance.message)
+
+The `pre_delete` and `post_delete` hooks no longer exist, and are replaced with `.perform_destroy(self, instance)`, which should delete the instance and perform any custom actions.
+
+ def perform_destroy(self, instance):
+ # Perform a custom pre-delete action.
+ send_deletion_alert(user=instance.created_by, deleted=instance)
+ # Delete the object instance.
+ instance.delete()
+
+#### Removal of view attributes.
+
+The `.object` and `.object_list` attributes are no longer set on the view instance. Treating views as mutable object instances that store state during the processing of the view tends to be poor design, and can lead to obscure flow logic.
+
+I would personally recommend that developers treat view instances as immutable objects in their application code.
+
+#### PUT as create.
+
+Allowing `PUT` as create operations is problematic, as it necessarily exposes information about the existence or non-existence of objects. It's also not obvious that transparently allowing re-creating of previously deleted instances is necessarily a better default behavior than simply returning `404` responses.
+
+Both styles "`PUT` as 404" and "`PUT` as create" can be valid in different circumstances, but we've now opted for the 404 behavior as the default, due to it being simpler and more obvious.
+
+If you need to restore the previous behavior you may want to include [this `AllowPUTAsCreateMixin` class](https://gist.github.com/tomchristie/a2ace4577eff2c603b1b) as a mixin to your views.
+
+#### Customizing error responses.
+
+The generic views now raise `ValidationFailed` exception for invalid data. This exception is then dealt with by the exception handler, rather than the view returning a `400 Bad Request` response directly.
+
+This change means that you can now easily customize the style of error responses across your entire API, without having to modify any of the generic views.
+
+---
+
+## The metadata API
+
+Behavior for dealing with `OPTIONS` requests was previously built directly into the class based views. This has now been properly separated out into a Metadata API that allows the same pluggable style as other API policies in REST framework.
+
+This makes it far easier to use a different style for `OPTIONS` responses throughout your API, and makes it possible to create third-party metadata policies.
+
+---
+
+## Serializers as HTML forms
+
+REST framework 3.0 includes templated HTML form rendering for serializers.
+
+This API should not yet be considered finalized, and will only be promoted to public API for the 3.1 release.
+
+Significant changes that you do need to be aware of include:
+
+* Nested HTML forms are now supported, for example, a `UserSerializer` with a nested `ProfileSerializer` will now render a nested `fieldset` when used in the browsable API.
+* Nested lists of HTML forms are not yet supported, but are planned for 3.1.
+* Because we now use templated HTML form generation, **the `widget` option is no longer available for serializer fields**. You can instead control the template that is used for a given field, by using the `style` dictionary.
+
+#### The `style` keyword argument for serializer fields.
+
+The `style` keyword argument can be used to pass through additional information from a serializer field, to the renderer class. In particular, the `HTMLFormRenderer` uses the `base_template` key to determine which template to render the field with.
+
+For example, to use a `textarea` control instead of the default `input` control, you would use the following…
+
+ additional_notes = serializers.CharField(
+ style={'base_template': 'textarea.html'}
+ )
+
+Similarly, to use a radio button control instead of the default `select` control, you would use the following…
+
+ color_channel = serializers.ChoiceField(
+ choices=['red', 'blue', 'green'],
+ style={'base_template': 'radio.html'}
+ )
+
+This API should be considered provisional, and there may be minor alterations with the incoming 3.1 release.
+
+---
+
+## API style
+
+There are some improvements in the default style we use in our API responses.
+
+#### Unicode JSON by default.
+
+Unicode JSON is now the default. The `UnicodeJSONRenderer` class no longer exists, and the `UNICODE_JSON` setting has been added. To revert this behavior use the new setting:
+
+ REST_FRAMEWORK = {
+ 'UNICODE_JSON': False
+ }
+
+#### Compact JSON by default.
+
+We now output compact JSON in responses by default. For example, we return:
+
+ {"email":"amy@example.com","is_admin":true}
+
+Instead of the following:
+
+ {"email": "amy@example.com", "is_admin": true}
+
+The `COMPACT_JSON` setting has been added, and can be used to revert this behavior if needed:
+
+ REST_FRAMEWORK = {
+ 'COMPACT_JSON': False
+ }
+
+#### File fields as URLs
+
+The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/dev/howto/static-files/#serving-uploaded-files-in-development).
+
+You can revert this behavior, and display filenames in the representation by using the `UPLOADED_FILES_USE_URL` settings key:
+
+ REST_FRAMEWORK = {
+ 'UPLOADED_FILES_USE_URL': False
+ }
+
+You can also modify serializer fields individually, using the `use_url` argument:
+
+ uploaded_file = serializers.FileField(use_url=False)
+
+Also note that you should pass the `request` object to the serializer as context when instantiating it, so that a fully qualified URL can be returned. Returned URLs will then be of the form `https://example.com/url_path/filename.txt`. For example:
+
+ context = {'request': request}
+ serializer = ExampleSerializer(instance, context=context)
+ return Response(serializer.data)
+
+If the request is omitted from the context, the returned URLs will be of the form `/url_path/filename.txt`.
+
+#### Throttle headers using `Retry-After`.
+
+The custom `X-Throttle-Wait-Second` header has now been dropped in favor of the standard `Retry-After` header. You can revert this behavior if needed by writing a custom exception handler for your application.
+
+#### Date and time objects as ISO-8859-1 strings in serializer data.
+
+Date and Time objects are now coerced to strings by default in the serializer output. Previously they were returned as `Date`, `Time` and `DateTime` objects, and later coerced to strings by the renderer.
+
+You can modify this behavior globally by settings the existing `DATE_FORMAT`, `DATETIME_FORMAT` and `TIME_FORMAT` settings keys. Setting these values to `None` instead of their default value of `'iso-8859-1'` will result in native objects being returned in serializer data.
+
+ REST_FRAMEWORK = {
+ # Return native `Date` and `Time` objects in `serializer.data`
+ 'DATETIME_FORMAT': None
+ 'DATE_FORMAT': None
+ 'TIME_FORMAT': None
+ }
+
+You can also modify serializer fields individually, using the `date_format`, `time_format` and `datetime_format` arguments:
+
+ # Return `DateTime` instances in `serializer.data`, not strings.
+ created = serializers.DateTimeField(format=None)
+
+#### Decimals as strings in serializer data.
+
+Decimals are now coerced to strings by default in the serializer output. Previously they were returned as `Decimal` objects, and later coerced to strings by the renderer.
+
+You can modify this behavior globally by using the `COERCE_DECIMAL_TO_STRING` settings key.
+
+ REST_FRAMEWORK = {
+ 'COERCE_DECIMAL_TO_STRING': False
+ }
+
+Or modify it on an individual serializer field, using the `coerce_to_string` keyword argument.
+
+ # Return `Decimal` instances in `serializer.data`, not strings.
+ amount = serializers.DecimalField(
+ max_digits=10,
+ decimal_places=2,
+ coerce_to_string=False
+ )
+
+The default JSON renderer will return float objects for un-coerced `Decimal` instances. This allows you to easily switch between string or float representations for decimals depending on your API design needs.
+
+---
+
+## Miscellaneous notes
+
+* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
+* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
+* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
+* `APIException` subclasses could previously take could previously take any arbitrary type in the `detail` argument. These exceptions now use translatable text strings, and as a result call `force_text` on the `detail` argument, which *must be a string*. If you need complex arguments to an `APIException` class, you should subclass it and override the `__init__()` method. Typically you'll instead want to use a custom exception handler to provide for non-standard error responses.
+
+---
+
+## What's coming next
+
+3.0 is an incremental release, and there are several upcoming features that will build on the baseline improvements that it makes.
+
+The 3.1 release is planned to address improvements in the following components:
+
+* Public API for using serializers as HTML forms.
+* Request parsing, mediatypes & the implementation of the browsable API.
+* Introduction of a new pagination API.
+* Better support for API versioning.
+
+The 3.2 release is planned to introduce an alternative admin-style interface to the browsable API.
+
+You can follow development on the GitHub site, where we use [milestones to indicate planning timescales](https://github.com/tomchristie/django-rest-framework/milestones).
+
+[kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3
+[sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors
+[mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py
+[django-localization]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files
diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md
new file mode 100644
index 00000000..80d4007e
--- /dev/null
+++ b/docs/topics/3.1-announcement.md
@@ -0,0 +1,209 @@
+# Django REST framework 3.1
+
+The 3.1 release is an intermediate step in the Kickstarter project releases, and includes a range of new functionality.
+
+Some highlights include:
+
+* A super-smart cursor pagination scheme.
+* An improved pagination API, supporting header or in-body pagination styles.
+* Pagination controls rendering in the browsable API.
+* Better support for API versioning.
+* Built-in internationalization support.
+* Support for Django 1.8's `HStoreField` and `ArrayField`.
+
+---
+
+## Pagination
+
+The pagination API has been improved, making it both easier to use, and more powerful.
+
+A guide to the headline features follows. For full details, see [the pagination documentation][pagination].
+
+Note that as a result of this work a number of settings keys and generic view attributes are now moved to pending deprecation. Controlling pagination styles is now largely handled by overriding a pagination class and modifying its configuration attributes.
+
+* The `PAGINATE_BY` settings key will continue to work but is now pending deprecation. The more obviously named `PAGE_SIZE` settings key should now be used instead.
+* The `PAGINATE_BY_PARAM`, `MAX_PAGINATE_BY` settings keys will continue to work but are now pending deprecation, in favor of setting configuration attributes on the configured pagination class.
+* The `paginate_by`, `page_query_param`, `paginate_by_param` and `max_paginate_by` generic view attributes will continue to work but are now pending deprecation, in favor of setting configuration attributes on the configured pagination class.
+* The `pagination_serializer_class` view attribute and `DEFAULT_PAGINATION_SERIALIZER_CLASS` settings key **are no longer valid**. The pagination API does not use serializers to determine the output format, and you'll need to instead override the `get_paginated_response` method on a pagination class in order to specify how the output format is controlled.
+
+#### New pagination schemes.
+
+Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default.
+
+The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject.
+
+#### Pagination controls in the browsable API.
+
+Paginated results now include controls that render directly in the browsable API. If you're using the page or limit/offset style, then you'll see a page based control displayed in the browsable API:
+
+![page number based pagination](../img/pages-pagination.png )
+
+The cursor based pagination renders a more simple style of control:
+
+![cursor based pagination](../img/cursor-pagination.png )
+
+#### Support for header-based pagination.
+
+The pagination API was previously only able to alter the pagination style in the body of the response. The API now supports being able to write pagination information in response headers, making it possible to use pagination schemes that use the `Link` or `Content-Range` headers.
+
+For more information, see the [custom pagination styles](../api-guide/pagination/#custom-pagination-styles) documentation.
+
+---
+
+## Versioning
+
+We've made it [easier to build versioned APIs][versioning]. Built-in schemes for versioning include both URL based and Accept header based variations.
+
+When using a URL based scheme, hyperlinked serializers will resolve relationships to the same API version as used on the incoming request.
+
+For example, when using `NamespaceVersioning`, and the following hyperlinked serializer:
+
+ class AccountsSerializer(serializer.HyperlinkedModelSerializer):
+ class Meta:
+ model = Accounts
+ fields = ('account_name', 'users')
+
+The output representation would match the version used on the incoming request. Like so:
+
+ GET http://example.org/v2/accounts/10 # Version 'v2'
+
+ {
+ "account_name": "europa",
+ "users": [
+ "http://example.org/v2/users/12", # Version 'v2'
+ "http://example.org/v2/users/54",
+ "http://example.org/v2/users/87"
+ ]
+ }
+
+---
+
+## Internationalization
+
+REST framework now includes a built-in set of translations, and [supports internationalized error responses][internationalization]. This allows you to either change the default language, or to allow clients to specify the language via the `Accept-Language` header.
+
+You can change the default language by using the standard Django `LANGUAGE_CODE` setting:
+
+ LANGUAGE_CODE = "es-es"
+
+You can turn on per-request language requests by adding `LocalMiddleware` to your `MIDDLEWARE_CLASSES` setting:
+
+ MIDDLEWARE_CLASSES = [
+ ...
+ 'django.middleware.locale.LocaleMiddleware'
+ ]
+
+When per-request internationalization is enabled, client requests will respect the `Accept-Language` header where possible. For example, let's make a request for an unsupported media type:
+
+**Request**
+
+ GET /api/users HTTP/1.1
+ Accept: application/xml
+ Accept-Language: es-es
+ Host: example.org
+
+**Response**
+
+ HTTP/1.0 406 NOT ACCEPTABLE
+
+ {
+ "detail": "No se ha podido satisfacer la solicitud de cabecera de Accept."
+ }
+
+Note that the structure of the error responses is still the same. We still have a `details` key in the response. If needed you can modify this behavior too, by using a [custom exception handler][custom-exception-handler].
+
+We include built-in translations both for standard exception cases, and for serializer validation errors.
+
+The full list of supported languages can be found on our [Transifex project page](https://www.transifex.com/projects/p/django-rest-framework/).
+
+If you only wish to support a subset of the supported languages, use Django's standard `LANGUAGES` setting:
+
+ LANGUAGES = [
+ ('de', _('German')),
+ ('en', _('English')),
+ ]
+
+For more details, see the [internationalization documentation](internationalization.md).
+
+Many thanks to [Craig Blaszczyk](https://github.com/jakul) for helping push this through.
+
+---
+
+## New field types
+
+Django 1.8's new `ArrayField`, `HStoreField` and `UUIDField` are now all fully supported.
+
+This work also means that we now have both `serializers.DictField()`, and `serializers.ListField()` types, allowing you to express and validate a wider set of representations.
+
+If you're building a new 1.8 project, then you should probably consider using `UUIDField` as the primary keys for all your models. This style will work automatically with hyperlinked serializers, returning URLs in the following style:
+
+ http://example.org/api/purchases/9b1a433f-e90d-4948-848b-300fdc26365d
+
+---
+
+## ModelSerializer API
+
+The serializer redesign in 3.0 did not include any public API for modifying how ModelSerializer classes automatically generate a set of fields from a given mode class. We've now re-introduced an API for this, allowing you to create new ModelSerializer base classes that behave differently, such as using a different default style for relationships.
+
+For more information, see the documentation on [customizing field mappings][customizing-field-mappings] for ModelSerializer classes.
+
+---
+
+## Moving packages out of core
+
+We've now moved a number of packages out of the core of REST framework, and into separately installable packages. If you're currently using these you don't need to worry, you simply need to `pip install` the new packages, and change any import paths.
+
+We're making this change in order to help distribute the maintainance workload, and keep better focus of the core essentials of the framework.
+
+The change also means we can be more flexible with which external packages we recommend. For example, the excellently maintained [Django OAuth toolkit](https://github.com/evonove/django-oauth-toolkit) has now been promoted as our recommended option for integrating OAuth support.
+
+The following packages are now moved out of core and should be separately installed:
+
+* OAuth - [djangorestframework-oauth](http://jpadilla.github.io/django-rest-framework-oauth/)
+* XML - [djangorestframework-xml](http://jpadilla.github.io/django-rest-framework-xml)
+* YAML - [djangorestframework-yaml](http://jpadilla.github.io/django-rest-framework-yaml)
+* JSONP - [djangorestframework-jsonp](http://jpadilla.github.io/django-rest-framework-jsonp)
+
+It's worth reiterating that this change in policy shouldn't mean any work in your codebase other than adding a new requirement and modifying some import paths. For example to install XML rendering, you would now do:
+
+ pip install djangorestframework-xml
+
+And modify your settings, like so:
+
+ REST_FRAMEWORK = {
+ 'DEFAULT_RENDERER_CLASSES': [
+ 'rest_framework.renderers.JSONRenderer',
+ 'rest_framework.renderers.BrowsableAPIRenderer',
+ 'rest_framework_xml.renderers.XMLRenderer'
+ ]
+ }
+
+Thanks go to the latest member of our maintenance team, [José Padilla](https://github.com/jpadilla/), for handling this work and taking on ownership of these packages.
+
+---
+
+## Deprecations
+
+The `request.DATA`, `request.FILES` and `request.QUERY_PARAMS` attributes move from pending deprecation, to deprecated. Use `request.data` and `request.query_params` instead, as discussed in the 3.0 release notes.
+
+The ModelSerializer Meta options for `write_only_fields`, `view_name` and `lookup_field` are also moved from pending deprecation, to deprecated. Use `extra_kwargs` instead, as discussed in the 3.0 release notes.
+
+All these attributes and options will still work in 3.1, but their usage will raise a warning. They will be fully removed in 3.2.
+
+---
+
+## What's next?
+
+The next focus will be on HTML renderings of API output and will include:
+
+* HTML form rendering of serializers.
+* Filtering controls built-in to the browsable API.
+* An alternative admin-style interface.
+
+This will either be made as a single 3.2 release, or split across two separate releases, with the HTML forms and filter controls coming in 3.2, and the admin-style interface coming in a 3.3 release.
+
+[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
+[pagination]: ../api-guide/pagination.md
+[versioning]: ../api-guide/versioning.md
+[internationalization]: internationalization.md
+[customizing-field-mappings]: ../api-guide/serializers.md#customizing-field-mappings
diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md
index 0555b84d..ad88810d 100644
--- a/docs/topics/ajax-csrf-cors.md
+++ b/docs/topics/ajax-csrf-cors.md
@@ -6,11 +6,11 @@
## Javascript clients
-If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
+If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
-AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`.
+AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`.
## CSRF protection
@@ -19,7 +19,7 @@ AJAX requests that are made on a different site from the API they are communicat
To guard against these type of attacks, you need to do two things:
1. Ensure that the 'safe' HTTP operations, such as `GET`, `HEAD` and `OPTIONS` cannot be used to alter any server-side state.
-2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token.
+2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token.
If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
@@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response
[cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
[csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
-[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
+[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
[cors]: http://www.w3.org/TR/cors/
[ottoyiu]: https://github.com/ottoyiu/
[django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/
diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md
index 2ae8cadb..2879db74 100644
--- a/docs/topics/browsable-api.md
+++ b/docs/topics/browsable-api.md
@@ -69,6 +69,7 @@ For more specific CSS tweaks than simply overriding the default bootstrap theme
All of the blocks available in the browsable API base template that can be used in your `api.html`.
+* `body` - The entire html `<body>`.
* `bodyclass` - Class attribute for the `<body>` tag, empty by default.
* `bootstrap_theme` - CSS for the Bootstrap theme.
* `bootstrap_navbar_variant` - CSS class for the navbar.
@@ -90,7 +91,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:
@@ -115,6 +116,7 @@ The context that's available to the template:
* `name` : The name of the resource
* `post_form` : A form instance for use by the POST form (if allowed)
* `put_form` : A form instance for use by the PUT form (if allowed)
+* `display_edit_forms` : A boolean indicating whether or not POST, PUT and PATCH forms will be displayed
* `request` : The request object
* `response` : The response object
* `version` : The version of Django REST Framework
@@ -122,38 +124,30 @@ The context that's available to the template:
* `FORMAT_PARAM` : The view can accept a format override
* `METHOD_PARAM` : The view can accept a method override
+You can override the `BrowsableAPIRenderer.get_context()` method to customise the context that gets passed to the template.
+
#### Not using base.html
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, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly. One solution is to replace the selector by an autocomplete widget, that only loads and renders a subset of the available options as needed.
-
-There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light]. To setup `django-autocomplete-light`, follow the [installation documentation][django-autocomplete-light-install], add the the following to the `api.html` template:
+#### Handling `ChoiceField` with large numbers of items.
- {% block script %}
- {{ block.super }}
- {% include 'autocomplete_light/static.html' %}
- {% endblock %}
-
-You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer field.
+When a relationship or `ChoiceField` has too many items, rendering the widget containing all the options can become very slow, and cause the browsable API rendering to perform poorly.
- import autocomplete_light
+The simplest option in this case is to replace the select input with a standard text input. For example:
- class BookSerializer(serializers.ModelSerializer):
- author = serializers.ChoiceField(
- widget=autocomplete_light.ChoiceWidget('AuthorAutocomplete')
- )
+ author = serializers.HyperlinkedRelatedField(
+ queryset=User.objects.all(),
+ style={'base_template': 'input.html'}
+ )
- class Meta:
- model = Book
+#### Autocomplete
----
+An alternative, but more complex option would be to replace the input with an autocomplete widget, that only loads and renders a subset of the available options as needed. If you need to do this you'll need to do some work to build a custom autocomplete HTML template yourself.
-![Autocomplete][autocomplete-image]
+There are [a variety of packages for autocomplete widgets][autocomplete-packages], such as [django-autocomplete-light][django-autocomplete-light], that you may want to refer to. Note that you will not be able to simply include these components as standard widgets, but will need to write the HTML template explicitly. This is because REST framework 3.0 no longer supports the `widget` keyword argument since it now uses templated HTML generation.
-*Screenshot of the autocomplete-light widget*
+Better support for autocomplete inputs is planned in future versions.
---
@@ -164,11 +158,10 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f
[bootstrap]: http://getbootstrap.com
[cerulean]: ../img/cerulean.png
[slate]: ../img/slate.png
-[bcustomize]: http://twitter.github.com/bootstrap/customize.html#variables
+[bcustomize]: http://getbootstrap.com/2.3.2/customize.html
[bswatch]: http://bootswatch.com/
-[bcomponents]: http://twitter.github.com/bootstrap/components.html
-[bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar
+[bcomponents]: http://getbootstrap.com/2.3.2/components.html
+[bcomponentsnav]: http://getbootstrap.com/2.3.2/components.html#navbar
[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
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 123e4a8a..c9626ebf 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -6,68 +6,143 @@
There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project.
-# Community
+## Community
-If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users.
+The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case.
-You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter.
+If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particular JavaScript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with.
-Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
+Other really great ways you can help move the community forward include helping to answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag.
-When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
+When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant.
+
+## Code of conduct
+
+Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome.
+
+Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations.
+
+The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums.
# Issues
-It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
+It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues].
Some tips on good issue reporting:
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
+* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintenance overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation.
+* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened.
+## Triaging issues
+Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to
-* TODO: Triage
+* Read through the ticket - does it make sense, is it missing any context that would help explain it better?
+* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group?
+* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request?
+* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package?
+* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again.
# Development
+To start developing on Django REST framework, clone the repo:
+
+ git clone git@github.com:tomchristie/django-rest-framework.git
+
+Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you set up your editor to automatically indicate non-conforming styles.
+
+## Testing
+
+To run the tests, clone the repository, and then:
+
+ # Setup the virtual environment
+ virtualenv env
+ source env/bin/activate
+ pip install -r requirements.txt
+
+ # Run the tests
+ ./runtests.py
+
+### Test options
+
+Run using a more concise output style.
+
+ ./runtests.py -q
+
+Run the tests using a more concise output style, no coverage, no flake8.
+
+ ./runtests.py --fast
+
+Don't run the flake8 code linting.
+
+ ./runtests.py --nolint
+
+Only run the flake8 code linting, don't run the tests.
+
+ ./runtests.py --lintonly
+
+Run the tests for a given test case.
-* git clone & PYTHONPATH
-* Pep8
-* Recommend editor that runs pep8
+ ./runtests.py MyTestCase
-### Pull requests
+Run the tests for a given test method.
-* Make pull requests early
-* Describe branching
+ ./runtests.py MyTestCase.test_this_method
-### Managing compatibility issues
+Shorter form to run the tests for a given test method.
-* Describe compat module
+ ./runtests.py test_this_method
-# Testing
+Note: The test case and test method matching is fuzzy and will sometimes run other tests that contain a partial string match to the given command line input.
-* Running the tests
-* tox
+### Running against multiple environments
+
+You can also use the excellent [tox][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
+
+ tox
+
+## Pull requests
+
+It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission.
+
+It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another separate issue without interfering with an ongoing pull requests.
+
+It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests.
+
+GitHub's documentation for working on pull requests is [available here][pull-requests].
+
+Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django.
+
+Once you've made a pull request take a look at the Travis build status in the GitHub interface and make sure the tests are running as you'd expect.
+
+![Travis status][travis-status]
+
+*Above: Travis build notifications*
+
+## Managing compatibility issues
+
+Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
# Documentation
The documentation for REST framework is built from the [Markdown][markdown] source files in [the docs directory][docs].
-There are many great markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
+There are many great Markdown editors that make working with the documentation really easy. The [Mou editor for Mac][mou] is one such editor that comes highly recommended.
## Building the documentation
-To build the documentation, simply run the `mkdocs.py` script.
+To build the documentation, install MkDocs with `pip install mkdocs` and then run the following command.
- ./mkdocs.py
+ mkdocs build
-This will build the html output into the `html` directory.
+This will build the documentation into the `site` directory.
-You can build the documentation and open a preview in a browser window by using the `-p` flag.
+You can build the documentation and open a preview in a browser window by using the `serve` command.
- ./mkdocs.py -p
+ mkdocs serve
## Language style
@@ -76,8 +151,7 @@ Documentation should be in American English. The tone of the documentation is v
Some other tips:
* Keep paragraphs reasonably short.
-* Use double spacing after the end of sentences.
-* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'.
+* Don't use abbreviations such as 'e.g.' but instead use the long form, such as 'For example'.
## Markdown style
@@ -88,8 +162,8 @@ There are a couple of conventions you should follow when working on the document
Headers should use the hash style. For example:
### Some important topic
-
-The underline style should not be used. **Don't do this:**
+
+The underline style should not be used. **Don't do this:**
Some important topic
====================
@@ -99,9 +173,9 @@ The underline style should not be used. **Don't do this:**
Links should always use the reference style, with the referenced hyperlinks kept at the end of the document.
Here is a link to [some other thing][other-thing].
-
+
More text...
-
+
[other-thing]: http://example.com/other/thing
This style helps keep the documentation source consistent and readable.
@@ -110,33 +184,28 @@ If you are hyperlinking to another REST framework document, you should use a rel
[authentication]: ../api-guide/authentication.md
-Linking in this style means you'll be able to click the hyperlink in your markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
+Linking in this style means you'll be able to click the hyperlink in your Markdown editor to open the referenced document. When the documentation is built, these links will be converted into regular links to HTML pages.
##### 3. Notes
If you want to draw attention to a note or warning, use a pair of enclosing lines, like so:
---
-
- **Note:** Make sure you do this thing.
-
- ---
-# Third party packages
+ **Note:** A useful documentation note.
-* Django reusable app
-
-# Core committers
+ ---
-* Still use pull reqs
-* Credits
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
-[github]: https://github.com/tomchristie/django-rest-framework
-[django-packages]: https://www.djangopackages.com/grids/g/api/
+[code-of-conduct]: https://www.djangoproject.com/conduct/
[google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[so-filter]: http://stackexchange.com/filters/66475/rest-framework
[issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open
+[pep-8]: http://www.python.org/dev/peps/pep-0008/
+[travis-status]: ../img/travis-status.png
+[pull-requests]: https://help.github.com/articles/using-pull-requests
+[tox]: http://tox.readthedocs.org/en/latest/
[markdown]: http://daringfireball.net/projects/markdown/basics
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
deleted file mode 100644
index 95cac717..00000000
--- a/docs/topics/credits.md
+++ /dev/null
@@ -1,340 +0,0 @@
-# Credits
-
-The following people have helped make REST framework great.
-
-* Tom Christie - [tomchristie]
-* Marko Tibold - [markotibold]
-* Paul Miller - [paulmillr]
-* Sébastien Piquemal - [sebpiq]
-* Carmen Wick - [cwick]
-* Alex Ehlke - [aehlke]
-* Alen Mujezinovic - [flashingpumpkin]
-* Carles Barrobés - [txels]
-* Michael Fötsch - [mfoetsch]
-* David Larlet - [david]
-* Andrew Straw - [astraw]
-* Zeth - [zeth]
-* Fernando Zunino - [fzunino]
-* Jens Alm - [ulmus]
-* Craig Blaszczyk - [jakul]
-* Garcia Solero - [garciasolero]
-* Tom Drummond - [devioustree]
-* Danilo Bargen - [dbrgn]
-* Andrew McCloud - [amccloud]
-* Thomas Steinacher - [thomasst]
-* Meurig Freeman - [meurig]
-* Anthony Nemitz - [anemitz]
-* Ewoud Kohl van Wijngaarden - [ekohl]
-* Michael Ding - [yandy]
-* Mjumbe Poe - [mjumbewu]
-* Natim - [natim]
-* Sebastian Żurek - [sebzur]
-* Benoit C - [dzen]
-* Chris Pickett - [bunchesofdonald]
-* Ben Timby - [btimby]
-* Michele Lazzeri - [michelelazzeri-nextage]
-* Camille Harang - [mammique]
-* Paul Oswald - [poswald]
-* Sean C. Farley - [scfarley]
-* Daniel Izquierdo - [izquierdo]
-* Can Yavuz - [tschan]
-* Shawn Lewis - [shawnlewis]
-* Alec Perkins - [alecperkins]
-* Michael Barrett - [phobologic]
-* Mathieu Dhondt - [laundromat]
-* Johan Charpentier - [cyberj]
-* Jamie Matthews - [j4mie]
-* Mattbo - [mattbo]
-* Max Hurl - [maximilianhurl]
-* Tomi Pajunen - [eofs]
-* Rob Dobson - [rdobson]
-* Daniel Vaca Araujo - [diviei]
-* Madis Väin - [madisvain]
-* Stephan Groß - [minddust]
-* Pavel Savchenko - [asfaltboy]
-* Otto Yiu - [ottoyiu]
-* Jacob Magnusson - [jmagnusson]
-* Osiloke Harold Emoekpere - [osiloke]
-* Michael Shepanski - [mjs7231]
-* Toni Michel - [tonimichel]
-* Ben Konrath - [benkonrath]
-* Marc Aymerich - [glic3rinu]
-* Ludwig Kraatz - [ludwigkraatz]
-* Rob Romano - [robromano]
-* Eugene Mechanism - [mechanism]
-* Jonas Liljestrand - [jonlil]
-* Justin Davis - [irrelative]
-* Dustin Bachrach - [dbachrach]
-* Mark Shirley - [maspwr]
-* Olivier Aubert - [oaubert]
-* Yuri Prezument - [yprez]
-* Fabian Buechler - [fabianbuechler]
-* Mark Hughes - [mhsparks]
-* Michael van de Waeter - [mvdwaeter]
-* Reinout van Rees - [reinout]
-* Michael Richards - [justanotherbody]
-* Ben Roberts - [roberts81]
-* Venkata Subramanian Mahalingam - [annacoder]
-* George Kappel - [gkappel]
-* Colin Murtaugh - [cmurtaugh]
-* Simon Pantzare - [pilt]
-* Szymon Teżewski - [sunscrapers]
-* Joel Marcotte - [joual]
-* Trey Hunner - [treyhunner]
-* Roman Akinfold - [akinfold]
-* Toran Billups - [toranb]
-* Sébastien Béal - [sebastibe]
-* Andrew Hankinson - [ahankinson]
-* Juan Riaza - [juanriaza]
-* Michael Mior - [michaelmior]
-* Marc Tamlyn - [mjtamlyn]
-* Richard Wackerbarth - [wackerbarth]
-* Johannes Spielmann - [shezi]
-* James Cleveland - [radiosilence]
-* Steve Gregory - [steve-gregory]
-* Federico Capoano - [nemesisdesign]
-* Bruno Renié - [brutasse]
-* Kevin Stone - [kevinastone]
-* Guglielmo Celata - [guglielmo]
-* Mike Tums - [mktums]
-* Michael Elovskikh - [wronglink]
-* Michał Jaworski - [swistakm]
-* Andrea de Marco - [z4r]
-* Fernando Rocha - [fernandogrd]
-* Xavier Ordoquy - [xordoquy]
-* Adam Wentz - [floppya]
-* Andreas Pelme - [pelme]
-* Ryan Detzel - [ryanrdetzel]
-* Omer Katz - [thedrow]
-* Wiliam Souza - [waa]
-* Jonas Braun - [iekadou]
-* Ian Dash - [bitmonkey]
-* Bouke Haarsma - [bouke]
-* Pierre Dulac - [dulaccc]
-* Dave Kuhn - [kuhnza]
-* Sitong Peng - [stoneg]
-* Victor Shih - [vshih]
-* Atle Frenvik Sveen - [atlefren]
-* J Paul Reed - [preed]
-* Matt Majewski - [forgingdestiny]
-* Jerome Chen - [chenjyw]
-* Andrew Hughes - [eyepulp]
-* Daniel Hepper - [dhepper]
-* Hamish Campbell - [hamishcampbell]
-* Marlon Bailey - [avinash240]
-* James Summerfield - [jsummerfield]
-* Andy Freeland - [rouge8]
-* Craig de Stigter - [craigds]
-* Pablo Recio - [pyriku]
-* Brian Zambrano - [brianz]
-* Òscar Vilaplana - [grimborg]
-* Ryan Kaskel - [ryankask]
-* Andy McKay - [andymckay]
-* Matteo Suppo - [matteosuppo]
-* Karol Majta - [lolek09]
-* David Jones - [commonorgarden]
-* Andrew Tarzwell - [atarzwell]
-* Michal Dvořák - [mikee2185]
-* Markus Törnqvist - [mjtorn]
-* Pascal Borreli - [pborreli]
-* Alex Burgel - [aburgel]
-* David Medina - [copitux]
-* Areski Belaid - [areski]
-* Ethan Freman - [mindlace]
-* David Sanders - [davesque]
-* Philip Douglas - [freakydug]
-* Igor Kalat - [trwired]
-* Rudolf Olah - [omouse]
-* Gertjan Oude Lohuis - [gertjanol]
-* Matthias Jacob - [cyroxx]
-* Pavel Zinovkin - [pzinovkin]
-* Will Kahn-Greene - [willkg]
-* Kevin Brown - [kevin-brown]
-* Rodrigo Martell - [coderigo]
-
-Many thanks to everyone who's contributed to the project.
-
-## Additional thanks
-
-The documentation is built with [Bootstrap] and [Markdown].
-
-Project hosting is with [GitHub].
-
-Continuous integration testing is managed with [Travis CI][travis-ci].
-
-The [live sandbox][sandbox] is hosted on [Heroku].
-
-Various inspiration taken from the [Rails], [Piston], [Tastypie], [Dagny] and [django-viewsets] projects.
-
-Development of REST framework 2.0 was sponsored by [DabApps].
-
-## Contact
-
-For usage questions please see the [REST framework discussion group][group].
-
-You can also contact [@_tomchristie][twitter] directly on twitter.
-
-[twitter]: http://twitter.com/_tomchristie
-[bootstrap]: http://twitter.github.com/bootstrap/
-[markdown]: http://daringfireball.net/projects/markdown/
-[github]: https://github.com/tomchristie/django-rest-framework
-[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework
-[rails]: http://rubyonrails.org/
-[piston]: https://bitbucket.org/jespern/django-piston
-[tastypie]: https://github.com/toastdriven/django-tastypie
-[dagny]: https://github.com/zacharyvoase/dagny
-[django-viewsets]: https://github.com/BertrandBordage/django-viewsets
-[dabapps]: http://lab.dabapps.com
-[sandbox]: http://restframework.herokuapp.com/
-[heroku]: http://www.heroku.com/
-[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
-
-[tomchristie]: https://github.com/tomchristie
-[markotibold]: https://github.com/markotibold
-[paulmillr]: https://github.com/paulmillr
-[sebpiq]: https://github.com/sebpiq
-[cwick]: https://github.com/cwick
-[aehlke]: https://github.com/aehlke
-[flashingpumpkin]: https://github.com/flashingpumpkin
-[txels]: https://github.com/txels
-[mfoetsch]: https://github.com/mfoetsch
-[david]: https://github.com/david
-[astraw]: https://github.com/astraw
-[zeth]: https://github.com/zeth
-[fzunino]: https://github.com/fzunino
-[ulmus]: https://github.com/ulmus
-[jakul]: https://github.com/jakul
-[garciasolero]: https://github.com/garciasolero
-[devioustree]: https://github.com/devioustree
-[dbrgn]: https://github.com/dbrgn
-[amccloud]: https://github.com/amccloud
-[thomasst]: https://github.com/thomasst
-[meurig]: https://github.com/meurig
-[anemitz]: https://github.com/anemitz
-[ekohl]: https://github.com/ekohl
-[yandy]: https://github.com/yandy
-[mjumbewu]: https://github.com/mjumbewu
-[natim]: https://github.com/natim
-[sebzur]: https://github.com/sebzur
-[dzen]: https://github.com/dzen
-[bunchesofdonald]: https://github.com/bunchesofdonald
-[btimby]: https://github.com/btimby
-[michelelazzeri-nextage]: https://github.com/michelelazzeri-nextage
-[mammique]: https://github.com/mammique
-[poswald]: https://github.com/poswald
-[scfarley]: https://github.com/scfarley
-[izquierdo]: https://github.com/izquierdo
-[tschan]: https://github.com/tschan
-[shawnlewis]: https://github.com/shawnlewis
-[alecperkins]: https://github.com/alecperkins
-[phobologic]: https://github.com/phobologic
-[laundromat]: https://github.com/laundromat
-[cyberj]: https://github.com/cyberj
-[j4mie]: https://github.com/j4mie
-[mattbo]: https://github.com/mattbo
-[maximilianhurl]: https://github.com/maximilianhurl
-[eofs]: https://github.com/eofs
-[rdobson]: https://github.com/rdobson
-[diviei]: https://github.com/diviei
-[madisvain]: https://github.com/madisvain
-[minddust]: https://github.com/minddust
-[asfaltboy]: https://github.com/asfaltboy
-[ottoyiu]: https://github.com/OttoYiu
-[jmagnusson]: https://github.com/jmagnusson
-[osiloke]: https://github.com/osiloke
-[mjs7231]: https://github.com/mjs7231
-[tonimichel]: https://github.com/tonimichel
-[benkonrath]: https://github.com/benkonrath
-[glic3rinu]: https://github.com/glic3rinu
-[ludwigkraatz]: https://github.com/ludwigkraatz
-[robromano]: https://github.com/robromano
-[mechanism]: https://github.com/mechanism
-[jonlil]: https://github.com/jonlil
-[irrelative]: https://github.com/irrelative
-[dbachrach]: https://github.com/dbachrach
-[maspwr]: https://github.com/maspwr
-[oaubert]: https://github.com/oaubert
-[yprez]: https://github.com/yprez
-[fabianbuechler]: https://github.com/fabianbuechler
-[mhsparks]: https://github.com/mhsparks
-[mvdwaeter]: https://github.com/mvdwaeter
-[reinout]: https://github.com/reinout
-[justanotherbody]: https://github.com/justanotherbody
-[roberts81]: https://github.com/roberts81
-[annacoder]: https://github.com/annacoder
-[gkappel]: https://github.com/gkappel
-[cmurtaugh]: https://github.com/cmurtaugh
-[pilt]: https://github.com/pilt
-[sunscrapers]: https://github.com/sunscrapers
-[joual]: https://github.com/joual
-[treyhunner]: https://github.com/treyhunner
-[akinfold]: https://github.com/akinfold
-[toranb]: https://github.com/toranb
-[sebastibe]: https://github.com/sebastibe
-[ahankinson]: https://github.com/ahankinson
-[juanriaza]: https://github.com/juanriaza
-[michaelmior]: https://github.com/michaelmior
-[mjtamlyn]: https://github.com/mjtamlyn
-[wackerbarth]: https://github.com/wackerbarth
-[shezi]: https://github.com/shezi
-[radiosilence]: https://github.com/radiosilence
-[steve-gregory]: https://github.com/steve-gregory
-[nemesisdesign]: https://github.com/nemesisdesign
-[brutasse]: https://github.com/brutasse
-[kevinastone]: https://github.com/kevinastone
-[guglielmo]: https://github.com/guglielmo
-[mktums]: https://github.com/mktums
-[wronglink]: https://github.com/wronglink
-[swistakm]: https://github.com/swistakm
-[z4r]: https://github.com/z4r
-[fernandogrd]: https://github.com/fernandogrd
-[xordoquy]: https://github.com/xordoquy
-[floppya]: https://github.com/floppya
-[pelme]: https://github.com/pelme
-[ryanrdetzel]: https://github.com/ryanrdetzel
-[thedrow]: https://github.com/thedrow
-[waa]: https://github.com/wiliamsouza
-[iekadou]: https://github.com/iekadou
-[bitmonkey]: https://github.com/bitmonkey
-[bouke]: https://github.com/bouke
-[dulaccc]: https://github.com/dulaccc
-[kuhnza]: https://github.com/kuhnza
-[stoneg]: https://github.com/stoneg
-[vshih]: https://github.com/vshih
-[atlefren]: https://github.com/atlefren
-[preed]: https://github.com/preed
-[forgingdestiny]: https://github.com/forgingdestiny
-[chenjyw]: https://github.com/chenjyw
-[eyepulp]: https://github.com/eyepulp
-[dhepper]: https://github.com/dhepper
-[hamishcampbell]: https://github.com/hamishcampbell
-[avinash240]: https://github.com/avinash240
-[jsummerfield]: https://github.com/jsummerfield
-[rouge8]: https://github.com/rouge8
-[craigds]: https://github.com/craigds
-[pyriku]: https://github.com/pyriku
-[brianz]: https://github.com/brianz
-[grimborg]: https://github.com/grimborg
-[ryankask]: https://github.com/ryankask
-[andymckay]: https://github.com/andymckay
-[matteosuppo]: https://github.com/matteosuppo
-[lolek09]: https://github.com/lolek09
-[commonorgarden]: https://github.com/commonorgarden
-[atarzwell]: https://github.com/atarzwell
-[mikee2185]: https://github.com/mikee2185
-[mjtorn]: https://github.com/mjtorn
-[pborreli]: https://github.com/pborreli
-[aburgel]: https://github.com/aburgel
-[copitux]: https://github.com/copitux
-[areski]: https://github.com/areski
-[mindlace]: https://github.com/mindlace
-[davesque]: https://github.com/davesque
-[freakydug]: https://github.com/freakydug
-[trwired]: https://github.com/trwired
-[omouse]: https://github.com/omouse
-[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
diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md
index 7ee538f5..d65e251f 100644
--- a/docs/topics/documenting-your-api.md
+++ b/docs/topics/documenting-your-api.md
@@ -16,7 +16,7 @@ The most common way to document Web APIs today is to produce documentation that
Marc Gibbons' [Django REST Swagger][django-rest-swagger] integrates REST framework with the [Swagger][swagger] API documentation tool. The package produces well presented API documentation, and includes interactive tools for testing API endpoints.
-The pacakge is fully documented, well supported, and comes highly recommended.
+The package is fully documented, well supported, and comes highly recommended.
Django REST Swagger supports REST framework versions 2.3 and above.
@@ -42,7 +42,7 @@ There are various other online tools and services for providing API documentatio
## Self describing APIs
-The browsable API that REST framwork provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser.
+The browsable API that REST framework provides makes it possible for your API to be entirely self describing. The documentation for each API endpoint can be provided simply by visiting the URL in your browser.
![Screenshot - Self describing API][image-self-describing-api]
@@ -54,7 +54,7 @@ The title that is used in the browsable API is generated from the view class nam
For example, the view `UserListView`, will be named `User List` when presented in the browsable API.
-When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`.
+When working with viewsets, an appropriate suffix is appended to each generated view. For example, the view set `UserViewSet` will generate views named `User List` and `User Instance`.
#### Setting the description
@@ -65,9 +65,9 @@ If the python `markdown` library is installed, then [markdown syntax][markdown]
class AccountListView(views.APIView):
"""
Returns a list of all **active** accounts in the system.
-
+
For more details on how accounts are activated please [see here][ref].
-
+
[ref]: http://example.com/activating-accounts
"""
@@ -84,7 +84,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me
def metadata(self, request):
"""
Don't include the view description in OPTIONS responses.
- """
+ """
data = super(ExampleView, self).metadata(request)
data.pop('description')
return data
@@ -93,11 +93,11 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me
## The hypermedia approach
-To be fully RESTful an API should present it's available actions as hypermedia controls in the responses that it sends.
+To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends.
-In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
+In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions that may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
-To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documention includes pointers to background reading, as well as links to various hypermedia formats.
+To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
diff --git a/docs/topics/internationalization.md b/docs/topics/internationalization.md
new file mode 100644
index 00000000..3968e23d
--- /dev/null
+++ b/docs/topics/internationalization.md
@@ -0,0 +1,113 @@
+# Internationalization
+
+> Supporting internationalization is not optional. It must be a core feature.
+>
+> &mdash; [Jannis Leidel, speaking at Django Under the Hood, 2015][cite].
+
+REST framework ships with translatable error messages. You can make these appear in your language enabling [Django's standard translation mechanisms][django-translation].
+
+Doing so will allow you to:
+
+* Select a language other than English as the default, using the standard `LANGUAGE_CODE` Django setting.
+* Allow clients to choose a language themselves, using the `LocaleMiddleware` included with Django. A typical usage for API clients would be to include an `Accept-Language` request header.
+
+## Enabling internationalized APIs
+
+You can change the default language by using the standard Django `LANGUAGE_CODE` setting:
+
+ LANGUAGE_CODE = "es-es"
+
+You can turn on per-request language requests by adding `LocalMiddleware` to your `MIDDLEWARE_CLASSES` setting:
+
+ MIDDLEWARE_CLASSES = [
+ ...
+ 'django.middleware.locale.LocaleMiddleware'
+ ]
+
+When per-request internationalization is enabled, client requests will respect the `Accept-Language` header where possible. For example, let's make a request for an unsupported media type:
+
+**Request**
+
+ GET /api/users HTTP/1.1
+ Accept: application/xml
+ Accept-Language: es-es
+ Host: example.org
+
+**Response**
+
+ HTTP/1.0 406 NOT ACCEPTABLE
+
+ {"detail": "No se ha podido satisfacer la solicitud de cabecera de Accept."}
+
+REST framework includes these built-in translations both for standard exception cases, and for serializer validation errors.
+
+Note that the translations only apply to the error strings themselves. The format of error messages, and the keys of field names will remain the same. An example `400 Bad Request` response body might look like this:
+
+ {"detail": {"username": ["Esse campo deve ser unico."]}}
+
+If you want to use different string for parts of the response such as `detail` and `non_field_errors` then you can modify this behavior by using a [custom exception handler][custom-exception-handler].
+
+#### Specifying the set of supported languages.
+
+By default all available languages will be supported.
+
+If you only wish to support a subset of the available languages, use Django's standard `LANGUAGES` setting:
+
+ LANGUAGES = [
+ ('de', _('German')),
+ ('en', _('English')),
+ ]
+
+## Adding new translations
+
+REST framework translations are managed online using [Transifex][transifex-project]. You can use the Transifex service to add new translation languages. The maintenance team will then ensure that these translation strings are included in the REST framework package.
+
+Sometimes you may need to add translation strings to your project locally. You may need to do this if:
+
+* You want to use REST Framework in a language which has not been translated yet on Transifex.
+* Your project includes custom error messages, which are not part of REST framework's default translation strings.
+
+#### Translating a new language locally
+
+This guide assumes you are already familiar with how to translate a Django app. If you're not, start by reading [Django's translation docs][django-translation].
+
+If you're translating a new language you'll need to translate the existing REST framework error messages:
+
+1. Make a new folder where you want to store the internationalization resources. Add this path to your [`LOCALE_PATHS`][django-locale-paths] setting.
+
+2. Now create a subfolder for the language you want to translate. The folder should be named using [locale name][django-locale-name] notation. For example: `de`, `pt_BR`, `es_AR`.
+
+3. Now copy the [base translations file][django-po-source] from the REST framework source code into your translations folder.
+
+4. Edit the `django.po` file you've just copied, translating all the error messages.
+
+5. Run `manage.py compilemessages -l pt_BR` to make the translations
+available for Django to use. You should see a message like `processing file django.po in <...>/locale/pt_BR/LC_MESSAGES`.
+
+6. Restart your development server to see the changes take effect.
+
+If you're only translating custom error messages that exist inside your project codebase you don't need to copy the REST framework source `django.po` file into a `LOCALE_PATHS` folder, and can instead simply run Django's standard `makemessages` process.
+
+## How the language is determined
+
+If you want to allow per-request language preferences you'll need to include `django.middleware.locale.LocaleMiddleware` in your `MIDDLEWARE_CLASSES` setting.
+
+You can find more information on how the language preference is determined in the [Django documentation][django-language-preference]. For reference, the method is:
+
+1. First, it looks for the language prefix in the requested URL.
+2. Failing that, it looks for the `LANGUAGE_SESSION_KEY` key in the current user’s session.
+3. Failing that, it looks for a cookie.
+4. Failing that, it looks at the `Accept-Language` HTTP header.
+5. Failing that, it uses the global `LANGUAGE_CODE` setting.
+
+For API clients the most appropriate of these will typically be to use the `Accept-Language` header; Sessions and cookies will not be available unless using session authentication, and generally better practice to prefer an `Accept-Language` header for API clients rather than using language URL prefixes.
+
+[cite]: http://youtu.be/Wa0VfS2q94Y
+[django-translation]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation
+[custom-exception-handler]: ../api-guide/exceptions.md#custom-exception-handling
+[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
+[django-po-source]: https://raw.githubusercontent.com/tomchristie/django-rest-framework/master/rest_framework/locale/en_US/LC_MESSAGES/django.po
+[django-language-preference]: https://docs.djangoproject.com/en/1.7/topics/i18n/translation/#how-django-discovers-language-preference
+[django-locale-paths]: https://docs.djangoproject.com/en/1.7/ref/settings/#std:setting-LOCALE_PATHS
+[django-locale-name]: https://docs.djangoproject.com/en/1.7/topics/i18n/#term-locale-name
+[contributing]: ../../CONTRIBUTING.md
diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md
new file mode 100644
index 00000000..78c5cce6
--- /dev/null
+++ b/docs/topics/kickstarter-announcement.md
@@ -0,0 +1,163 @@
+# Kickstarting Django REST framework 3
+
+---
+
+<iframe style="display: block; margin: 0 auto 0 auto" width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe>
+
+---
+
+In order to continue to drive the project forward, I'm launching a Kickstarter campaign to help fund the development of a major new release - Django REST framework 3.
+
+## Project details
+
+This new release will allow us to comprehensively address some of the shortcomings of the framework, and will aim to include the following:
+
+* Faster, simpler and easier-to-use serializers.
+* An alternative admin-style interface for the browsable API.
+* Search and filtering controls made accessible in the browsable API.
+* Alternative API pagination styles.
+* Documentation around API versioning.
+* Triage of outstanding tickets.
+* Improving the ongoing quality and maintainability of the project.
+
+Full details are available now on the [project page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3).
+
+If you're interested in helping make sustainable open source development a reality please [visit the Kickstarter page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding the project.
+
+I can't wait to see where this takes us!
+
+Many thanks to everyone for your support so far,
+
+ Tom Christie :)
+
+---
+
+## Sponsors
+
+We've now blazed way past all our goals, with a staggering £30,000 (~$50,000), meaning I'll be in a position to work on the project significantly beyond what we'd originally planned for. I owe a huge debt of gratitude to all the wonderful companies and individuals who have been backing the project so generously, and making this possible.
+
+---
+
+### Platinum sponsors
+
+Our platinum sponsors have each made a hugely substantial contribution to the future development of Django REST framework, and I simply can't thank them enough.
+
+<ul class="sponsor diamond">
+<li><a href="https://www.eventbrite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/0-eventbrite.png);">Eventbrite</a></li>
+</ul>
+
+<ul class="sponsor platinum">
+<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../../img/sponsors/1-divio.png);">Divio</a></li>
+<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../../img/sponsors/1-lulu.png);">Lulu</a></li>
+<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../../img/sponsors/1-potato.png);">Potato</a></li>
+<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-wiredrive.png);">Wiredrive</a></li>
+<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-cyan.png);">Cyan</a></li>
+<li><a href="https://www.runscope.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-runscope.png);">Runscope</a></li>
+<li><a href="http://simpleenergy.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-simple-energy.png);">Simple Energy</a></li>
+<li><a href="http://vokalinteractive.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-vokal_interactive.png);">VOKAL Interactive</a></li>
+<li><a href="http://www.purplebit.com/" rel="nofollow" style="background-image:url(../../img/sponsors/1-purplebit.png);">Purple Bit</a></li>
+<li><a href="http://www.kuwaitnet.net/" rel="nofollow" style="background-image:url(../../img/sponsors/1-kuwaitnet.png);">KuwaitNET</a></li>
+</ul>
+
+<div style="clear: both"></div>
+
+---
+
+### Gold sponsors
+
+Our gold sponsors include companies large and small. Many thanks for their significant funding of the project and their commitment to sustainable open-source development.
+
+<ul class="sponsor gold">
+<li><a href="https://laterpay.net/" rel="nofollow" style="background-image:url(../../img/sponsors/2-laterpay.png);">LaterPay</a></li>
+<li><a href="https://www.schubergphilis.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-schuberg_philis.png);">Schuberg Philis</a></li>
+<li><a href="http://prorenata.se/" rel="nofollow" style="background-image:url(../../img/sponsors/2-prorenata.png);">ProReNata AB</a></li>
+<li><a href="https://www.sgawebsites.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-sga.png);">SGA Websites</a></li>
+<li><a href="http://www.sirono.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-sirono.png);">Sirono</a></li>
+<li><a href="http://www.vinta.com.br/" rel="nofollow" style="background-image:url(../../img/sponsors/2-vinta.png);">Vinta Software Studio</a></li>
+<li><a href="http://www.rapasso.nl/index.php/en" rel="nofollow" style="background-image:url(../../img/sponsors/2-rapasso.png);">Rapasso</a></li>
+<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-mirus_research.png);">Mirus Research</a></li>
+<li><a href="http://hipolabs.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipo.png);">Hipo</a></li>
+<li><a href="http://www.byte.nl" rel="nofollow" style="background-image:url(../../img/sponsors/2-byte.png);">Byte</a></li>
+<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
+<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-opbeat.png);">Opbeat</a></li>
+<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-koordinates.png);">Koordinates</a></li>
+<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
+<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
+<li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-heroku.png);">Heroku</a></li>
+<li><a href="https://www.rheinwerk-verlag.de/" rel="nofollow" style="background-image:url(../../img/sponsors/2-rheinwerk_verlag.png);">Rheinwerk Verlag</a></li>
+<li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-security_compass.png);">Security Compass</a></li>
+<li><a href="https://www.djangoproject.com/foundation/" rel="nofollow" style="background-image:url(../../img/sponsors/2-django.png);">Django Software Foundation</a></li>
+<li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipflask.png);">Hipflask</a></li>
+<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../../img/sponsors/2-crate.png);">Crate</a></li>
+<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li>
+<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-nexthub.png);">NextHub</a></li>
+<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-compile.png);">Compile</a></li>
+<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../../img/sponsors/2-wusawork.png);">WusaWork</a></li>
+<li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li>
+</ul>
+
+<div style="clear: both; padding-bottom: 40px;"></div>
+
+---
+
+### Silver sponsors
+
+The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank&nbsp;you to individuals who have choosen to privately support the project at this level.
+
+<ul class="sponsor silver">
+<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
+<li><a href="http://wildfish.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-wildfish.png);">Wildfish</a></li>
+<li><a href="http://www.thermondo.de/" rel="nofollow" style="background-image:url(../../img/sponsors/3-thermondo-gmbh.png);">Thermondo GmbH</a></li>
+<li><a href="http://providenz.fr/" rel="nofollow" style="background-image:url(../../img/sponsors/3-providenz.png);">Providenz</a></li>
+<li><a href="https://www.alwaysdata.com" rel="nofollow" style="background-image:url(../../img/sponsors/3-alwaysdata.png);">alwaysdata.com</a></li>
+<li><a href="http://www.triggeredmessaging.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-triggered_messaging.png);">Triggered Messaging</a></li>
+<li><a href="https://www.ipushpull.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-ipushpull.png);">PushPull Technology Ltd</a></li>
+<li><a href="http://www.transcode.de/" rel="nofollow" style="background-image:url(../../img/sponsors/3-transcode.png);">Transcode</a></li>
+<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-garfo.png);">Garfo</a></li>
+<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-shippo.png);">Shippo</a></li>
+<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-gizmag.png);">Gizmag</a></li>
+<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-tivix.png);">Tivix</a></li>
+<li><a href="http://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-safari.png);">Safari</a></li>
+<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-brightloop.png);">Bright Loop</a></li>
+<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aba.png);">ABA Systems</a></li>
+<li><a href="http://beefarm.ru/" rel="nofollow" style="background-image:url(../../img/sponsors/3-beefarm.png);">beefarm.ru</a></li>
+<li><a href="http://www.vzzual.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-vzzual.png);">Vzzual.com</a></li>
+<li><a href="http://infinite-code.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-infinite_code.png);">Infinite Code</a></li>
+<li><a href="http://crosswordtracker.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-crosswordtracker.png);">Crossword Tracker</a></li>
+<li><a href="https://www.pkgfarm.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-pkgfarm.png);">PkgFarm</a></li>
+<li><a href="http://life.tl/" rel="nofollow" style="background-image:url(../../img/sponsors/3-life_the_game.png);">Life. The Game.</a></li>
+<li><a href="http://blimp.io/" rel="nofollow" style="background-image:url(../../img/sponsors/3-blimp.png);">Blimp</a></li>
+<li><a href="http://pathwright.com" rel="nofollow" style="background-image:url(../../img/sponsors/3-pathwright.png);">Pathwright</a></li>
+<li><a href="http://fluxility.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-fluxility.png);">Fluxility</a></li>
+<li><a href="http://teonite.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-teonite.png);">Teonite</a></li>
+<li><a href="http://trackmaven.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-trackmaven.png);">TrackMaven</a></li>
+<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../../img/sponsors/3-phurba.png);">Phurba</a></li>
+<li><a href="http://www.nephila.co.uk/" rel="nofollow" style="background-image:url(../../img/sponsors/3-nephila.png);">Nephila</a></li>
+<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-aditium.png);">Aditium</a></li>
+<li><a href="http://www.eyesopen.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li>
+<li><a href="https://holvi.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-holvi.png);">Holvi</a></li>
+<li><a href="http://cantemo.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-cantemo.gif);">Cantemo</a></li>
+<li><a href="https://www.makespace.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-makespace.png);">MakeSpace</a></li>
+<li><a href="https://www.ax-semantics.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-ax_semantics.png);">AX Semantics</a></li>
+<li><a href="http://istrategylabs.com/" rel="nofollow" style="background-image:url(../../img/sponsors/3-isl.png);">ISL</a></li>
+</ul>
+
+<div style="clear: both; padding-bottom: 40px;"></div>
+
+**Individual backers**: Paul Hallett, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen.
+
+---
+
+### Advocates
+
+The following individuals made a significant financial contribution to the development of Django REST framework 3, for which I can only offer a huge, warm and sincere thank you!
+
+**Individual backers**: Jure Cuhalev, Kevin Brolly, Ferenc Szalai, Dougal Matthews, Stefan Foulis, Carlos Hernando, Alen Mujezinovic, Ross Crawford-d'Heureuse, George Kappel, Alasdair Nicol, John Carr, Steve Winton, Trey, Manuel Miranda, David Horn, Vince Mi, Daniel Sears, Jamie Matthews, Ryan Currah, Marty Kemka, Scott Nixon, Moshin Elahi, Kevin Campbell, Jose Antonio Leiva Izquierdo, Kevin Stone, Andrew Godwin, Tijs Teulings, Roger Boardman, Xavier Antoviaque, Darian Moody, Lujeni, Jon Dugan, Wiley Kestner, Daniel C. Silverstein, Daniel Hahler, Subodh Nijsure, Philipp Weidenhiller, Yusuke Muraoka, Danny Roa, Reto Aebersold, Kyle Getrost, Décébal Hormuz, James Dacosta, Matt Long, Mauro Rocco, Tyrel Souza, Ryan Campbell, Ville Jyrkkä, Charalampos Papaloizou, Nikolai Røed Kristiansen, Antoni Aloy López, Celia Oakley, Michał Krawczak, Ivan VenOsdel, Tim Watts, Martin Warne, Nicola Jordan, Ryan Kaskel.
+
+**Corporate backers**: Savannah Informatics, Prism Skylabs, Musical Operating Devices.
+
+---
+
+### Supporters
+
+There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you!
diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md
new file mode 100644
index 00000000..4926f355
--- /dev/null
+++ b/docs/topics/project-management.md
@@ -0,0 +1,200 @@
+# Project management
+
+> "No one can whistle a symphony; it takes a whole orchestra to play it"
+>
+> &mdash; Halford E. Luccock
+
+This document outlines our project management processes for REST framework.
+
+The aim is to ensure that the project has a high
+["bus factor"][bus-factor], and can continue to remain well supported for the foreseeable future. Suggestions for improvements to our process are welcome.
+
+---
+
+## Maintenance team
+
+We have a quarterly maintenance cycle where new members may join the maintenance team. We currently cap the size of the team at 5 members, and may encourage folks to step out of the team for a cycle to allow new members to participate.
+
+#### Current team
+
+The [maintenance team for Q1 2015](https://github.com/tomchristie/django-rest-framework/issues/2190):
+
+* [@tomchristie](https://github.com/tomchristie/)
+* [@xordoquy](https://github.com/xordoquy/) (Release manager.)
+* [@carltongibson](https://github.com/carltongibson/)
+* [@kevin-brown](https://github.com/kevin-brown/)
+* [@jpadilla](https://github.com/jpadilla/)
+
+#### Maintenance cycles
+
+Each maintenance cycle is initiated by an issue being opened with the `Process` label.
+
+* To be considered for a maintainer role simply comment against the issue.
+* Existing members must explicitly opt-in to the next cycle by check-marking their name.
+* The final decision on the incoming team will be made by `@tomchristie`.
+
+Members of the maintenance team will be added as collaborators to the repository.
+
+The following template should be used for the description of the issue, and serves as the formal process for selecting the team.
+
+ This issue is for determining the maintenance team for the *** period.
+
+ Please see the [Project management](http://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details.
+
+ ---
+
+ #### Renewing existing members.
+
+ The following people are the current maintenance team. Please checkmark your name if you wish to continue to have write permission on the repository for the *** period.
+
+ - [ ] @***
+ - [ ] @***
+ - [ ] @***
+ - [ ] @***
+ - [ ] @***
+
+ ---
+
+ #### New members.
+
+ If you wish to be considered for this or a future date, please comment against this or subsequent issues.
+
+ To modify this process for future maintenance cycles make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
+
+#### Responsibilities of team members
+
+Team members have the following responsibilities.
+
+* Close invalid or resolved tickets.
+* Add triage labels and milestones to tickets.
+* Merge finalized pull requests.
+* Build and deploy the documentation, using `mkdocs gh-deploy`.
+* Build and update the included translation packs.
+
+Further notes for maintainers:
+
+* Code changes should come in the form of a pull request - do not push directly to master.
+* Maintainers should typically not merge their own pull requests.
+* Each issue/pull request should have exactly one label once triaged.
+* Search for un-triaged issues with [is:open no:label][un-triaged].
+
+It should be noted that participating actively in the REST framework project clearly **does not require being part of the maintenance team**. Almost every import part of issue triage and project improvement can be actively worked on regardless of your collaborator status on the repository.
+
+---
+
+## Release process
+
+The release manager is selected on every quarterly maintenance cycle.
+
+* The manager should be selected by `@tomchristie`.
+* The manager will then have the maintainer role added to PyPI package.
+* The previous manager will then have the maintainer role removed from the PyPI package.
+
+Our PyPI releases will be handled by either the current release manager, or by `@tomchristie`. Every release should have an open issue tagged with the `Release` label and marked against the appropriate milestone.
+
+The following template should be used for the description of the issue, and serves as a release checklist.
+
+ Release manager is @***.
+ Pull request is #***.
+
+ Checklist:
+
+ - [ ] Create pull request for [release notes](https://github.com/tomchristie/django-rest-framework/blob/master/docs/topics/release-notes.md) based on the [*.*.* milestone](https://github.com/tomchristie/django-rest-framework/milestones/***).
+ - [ ] Ensure the pull request increments the version to `*.*.*` in [`restframework/__init__.py`](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/__init__.py).
+ - [ ] Confirm with @tomchristie that release is finalized and ready to go.
+ - [ ] Ensure that release date is included in pull request.
+ - [ ] Merge the release pull request.
+ - [ ] Push the package to PyPI with `./setup.py publish`.
+ - [ ] Tag the release, with `git tag -a *.*.* -m 'version *.*.*'; git push --tags`.
+ - [ ] Deploy the documentation with `mkdocs gh-deploy`.
+ - [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework).
+ - [ ] Make a release announcement on twitter.
+ - [ ] Close the milestone on GitHub.
+
+ To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation.
+
+When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages.
+
+---
+
+## Translations
+
+The maintenance team are responsible for managing the translation packs include in REST framework. Translating the source strings into multiple languages is managed through the [transifex service][transifex-project].
+
+### Managing Transifex
+
+The [official Transifex client][transifex-client] is used to upload and download translations to Transifex. The client is installed using pip:
+
+ pip install transifex-client
+
+To use it you'll need a login to Transifex which has a password, and you'll need to have administrative access to the Transifex project. You'll need to create a `~/.transifexrc` file which contains your credentials.
+
+ [https://www.transifex.com]
+ username = ***
+ token = ***
+ password = ***
+ hostname = https://www.transifex.com
+
+### Upload new source files
+
+When any user visible strings are changed, they should be uploaded to Transifex so that the translators can start to translate them. To do this, just run:
+
+ # 1. Update the source django.po file, which is the US English version.
+ cd rest_framework
+ django-admin.py makemessages -l en_US
+ # 2. Push the source django.po file to Transifex.
+ cd ..
+ tx push -s
+
+When pushing source files, Transifex will update the source strings of a resource to match those from the new source file.
+
+Here's how differences between the old and new source files will be handled:
+
+* New strings will be added.
+* Modified strings will be added as well.
+* Strings which do not exist in the new source file will be removed from the database, along with their translations. If that source strings gets re-added later then [Transifex Translation Memory][translation-memory] will automatically include the translation string.
+
+### Download translations
+
+When a translator has finished translating their work needs to be downloaded from Transifex into the REST framework repository. To do this, run:
+
+ # 3. Pull the translated django.po files from Transifex.
+ tx pull -a
+ cd rest_framework
+ # 4. Compile the binary .mo files for all supported languages.
+ django-admin.py compilemessages
+
+---
+
+## Project requirements
+
+All our test requirements are pinned to exact versions, in order to ensure that our test runs are reproducible. We maintain the requirements in the `requirements` directory. The requirements files are referenced from the `tox.ini` configuration file, ensuring we have a single source of truth for package versions used in testing.
+
+Package upgrades should generally be treated as isolated pull requests. You can check if there are any packages available at a newer version, by using the `pip list --outdated`.
+
+---
+
+## Project ownership
+
+The PyPI package is owned by `@tomchristie`. As a backup `@j4mie` also has ownership of the package.
+
+If `@tomchristie` ceases to participate in the project then `@j4mie` has responsibility for handing over ownership duties.
+
+#### Outstanding management & ownership issues
+
+The following issues still need to be addressed:
+
+* [Consider moving the repo into a proper GitHub organization][github-org].
+* Ensure `@jamie` has back-up access to the `django-rest-framework.org` domain setup and admin.
+* Document ownership of the [live example][sandbox] API.
+* Document ownership of the [mailing list][mailing-list] and IRC channel.
+* Document ownership and management of the security mailing list.
+
+[bus-factor]: http://en.wikipedia.org/wiki/Bus_factor
+[un-triaged]: https://github.com/tomchristie/django-rest-framework/issues?q=is%3Aopen+no%3Alabel
+[transifex-project]: https://www.transifex.com/projects/p/django-rest-framework/
+[transifex-client]: https://pypi.python.org/pypi/transifex-client
+[translation-memory]: http://docs.transifex.com/guides/tm#let-tm-automatically-populate-translations
+[github-org]: https://github.com/tomchristie/django-rest-framework/issues/2162
+[sandbox]: http://restframework.herokuapp.com/
+[mailing-list]: https://groups.google.com/forum/#!forum/django-rest-framework
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index d379ab74..35592feb 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -10,7 +10,7 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You
Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
-Major version numbers (x.0.0) are reserved for substantial project milestones. No major point releases are currently planned.
+Major version numbers (x.0.0) are reserved for substantial project milestones.
## Deprecation policy
@@ -38,516 +38,224 @@ You can determine your currently installed version using `pip freeze`:
---
-## 2.3.x series
-
-### 2.3.6
-
-**Date**: 27th June 2013
-
-* Added `trailing_slash` option to routers.
-* Include support for `HttpStreamingResponse`.
-* Support wider range of default serializer validation when used with custom model fields.
-* UTF-8 Support for browsable API descriptions.
-* OAuth2 provider uses timezone aware datetimes when supported.
-* Bugfix: Return error correctly when OAuth non-existent consumer occurs.
-* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg.
-* Bugfix: Fix `ScopedRateThrottle`.
-
-### 2.3.5
-
-**Date**: 3rd June 2013
-
-* Added `get_url` hook to `HyperlinkedIdentityField`.
-* Serializer field `default` argument may be a callable.
-* `@action` decorator now accepts a `methods` argument.
-* Bugfix: `request.user` should be still be accessible in renderer context if authentication fails.
-* Bugfix: The `lookup_field` option on `HyperlinkedIdentityField` should apply by default to the url field on the serializer.
-* Bugfix: `HyperlinkedIdentityField` should continue to support `pk_url_kwarg`, `slug_url_kwarg`, `slug_field`, in a pending deprecation state.
-* Bugfix: Ensure we always return 404 instead of 500 if a lookup field cannot be converted to the correct lookup type. (Eg non-numeric `AutoInteger` pk lookup)
-
-### 2.3.4
-
-**Date**: 24th May 2013
-
-* Serializer fields now support `label` and `help_text`.
-* Added `UnicodeJSONRenderer`.
-* `OPTIONS` requests now return metadata about fields for `POST` and `PUT` requests.
-* Bugfix: `charset` now properly included in `Content-Type` of responses.
-* Bugfix: Blank choice now added in browsable API on nullable relationships.
-* Bugfix: Many to many relationships with `through` tables are now read-only.
-* Bugfix: Serializer fields now respect model field args such as `max_length`.
-* Bugfix: SlugField now performs slug validation.
-* Bugfix: Lazy-translatable strings now properly serialized.
-* Bugfix: Browsable API now supports bootswatch styles properly.
-* Bugfix: HyperlinkedIdentityField now uses `lookup_field` kwarg.
-
-**Note**: Responses now correctly include an appropriate charset on the `Content-Type` header. For example: `application/json; charset=utf-8`. If you have tests that check the content type of responses, you may need to update these accordingly.
-
-### 2.3.3
-
-**Date**: 16th May 2013
-
-* Added SearchFilter
-* Added OrderingFilter
-* Added GenericViewSet
-* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
-* Bugfix: Fix API Root view issue with DjangoModelPermissions
-
-### 2.3.2
-
-**Date**: 8th May 2013
-
-* Bugfix: Fix `TIME_FORMAT`, `DATETIME_FORMAT` and `DATE_FORMAT` settings.
-* Bugfix: Fix `DjangoFilterBackend` issue, failing when used on view with queryset attribute.
-
-### 2.3.1
-
-**Date**: 7th May 2013
-
-* Bugfix: Fix breadcrumb rendering issue.
-
-### 2.3.0
-
-**Date**: 7th May 2013
-
-* ViewSets and Routers.
-* ModelSerializers support reverse relations in 'fields' option.
-* HyperLinkedModelSerializers support 'id' field in 'fields' option.
-* Cleaner generic views.
-* Support for multiple filter classes.
-* FileUploadParser support for raw file uploads.
-* DecimalField support.
-* Made Login template easier to restyle.
-* Bugfix: Fix issue with depth>1 on ModelSerializer.
-
-**Note**: See the [2.3 announcement][2.3-announcement] for full details.
-
----
-
-## 2.2.x series
-
-### 2.2.7
-
-**Date**: 17th April 2013
-
-* Loud failure when view does not return a `Response` or `HttpResponse`.
-* Bugfix: Fix for Django 1.3 compatibility.
-* Bugfix: Allow overridden `get_object()` to work correctly.
-
-### 2.2.6
-
-**Date**: 4th April 2013
-
-* OAuth2 authentication no longer requires unnecessary URL parameters in addition to the token.
-* URL hyperlinking in browsable API now handles more cases correctly.
-* Long HTTP headers in browsable API are broken in multiple lines when possible.
-* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
-* Bugfix: OAuth should fail hard when invalid token used.
-* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`.
-
-### 2.2.5
-
-**Date**: 26th March 2013
-
-* Serializer support for bulk create and bulk update operations.
-* Regression fix: Date and time fields return date/time objects by default. Fixes regressions caused by 2.2.2. See [#743][743] for more details.
-* Bugfix: Fix 500 error is OAuth not attempted with OAuthAuthentication class installed.
-* `Serializer.save()` now supports arbitrary keyword args which are passed through to the object `.save()` method. Mixins use `force_insert` and `force_update` where appropriate, resulting in one less database query.
-
-### 2.2.4
-
-**Date**: 13th March 2013
-
-* OAuth 2 support.
-* OAuth 1.0a support.
-* Support X-HTTP-Method-Override header.
-* Filtering backends are now applied to the querysets for object lookups as well as lists. (Eg you can use a filtering backend to control which objects should 404)
-* Deal with error data nicely when deserializing lists of objects.
-* Extra override hook to configure `DjangoModelPermissions` for unauthenticated users.
-* Bugfix: Fix regression which caused extra database query on paginated list views.
-* Bugfix: Fix pk relationship bug for some types of 1-to-1 relations.
-* Bugfix: Workaround for Django bug causing case where `Authtoken` could be registered for cascade delete from `User` even if not installed.
-
-### 2.2.3
-
-**Date**: 7th March 2013
-
-* Bugfix: Fix None values for for `DateField`, `DateTimeField` and `TimeField`.
-
-### 2.2.2
-
-**Date**: 6th March 2013
-
-* Support for custom input and output formats for `DateField`, `DateTimeField` and `TimeField`.
-* Cleanup: Request authentication is no longer lazily evaluated, instead authentication is always run, which results in more consistent, obvious behavior. Eg. Supplying bad auth credentials will now always return an error response, even if no permissions are set on the view.
-* Bugfix for serializer data being uncacheable with pickle protocol 0.
-* Bugfixes for model field validation edge-cases.
-* Bugfix for authtoken migration while using a custom user model and south.
-
-### 2.2.1
-
-**Date**: 22nd Feb 2013
-
-* Security fix: Use `defusedxml` package to address XML parsing vulnerabilities.
-* Raw data tab added to browsable API. (Eg. Allow for JSON input.)
-* Added TimeField.
-* Serializer fields can be mapped to any method that takes no args, or only takes kwargs which have defaults.
-* Unicode support for view names/descriptions in browsable API.
-* Bugfix: request.DATA should return an empty `QueryDict` with no data, not `None`.
-* Bugfix: Remove unneeded field validation, which caused extra queries.
-
-**Security note**: Following the [disclosure of security vulnerabilities][defusedxml-announce] in Python's XML parsing libraries, use of the `XMLParser` class now requires the `defusedxml` package to be installed.
-
-The security vulnerabilities only affect APIs which use the `XMLParser` class, by enabling it in any views, or by having it set in the `DEFAULT_PARSER_CLASSES` setting. Note that the `XMLParser` class is not enabled by default, so this change should affect a minority of users.
-
-### 2.2.0
-
-**Date**: 13th Feb 2013
-
-* Python 3 support.
-* Added a `post_save()` hook to the generic views.
-* Allow serializers to handle dicts as well as objects.
-* Deprecate `ManyRelatedField()` syntax in favor of `RelatedField(many=True)`
-* Deprecate `null=True` on relations in favor of `required=False`.
-* Deprecate `blank=True` on CharFields, just use `required=False`.
-* Deprecate optional `obj` argument in permissions checks in favor of `has_object_permission`.
-* Deprecate implicit hyperlinked relations behavior.
-* Bugfix: Fix broken DjangoModelPermissions.
-* Bugfix: Allow serializer output to be cached.
-* Bugfix: Fix styling on browsable API login.
-* Bugfix: Fix issue with deserializing empty to-many relations.
-* Bugfix: Ensure model field validation is still applied for ModelSerializer subclasses with an custom `.restore_object()` method.
-
-**Note**: See the [2.2 announcement][2.2-announcement] for full details.
-
----
-
-## 2.1.x series
-
-### 2.1.17
-
-**Date**: 26th Jan 2013
-
-* Support proper 401 Unauthorized responses where appropriate, instead of always using 403 Forbidden.
-* Support json encoding of timedelta objects.
-* `format_suffix_patterns()` now supports `include` style URL patterns.
-* Bugfix: Fix issues with custom pagination serializers.
-* Bugfix: Nested serializers now accept `source='*'` argument.
-* Bugfix: Return proper validation errors when incorrect types supplied for relational fields.
-* Bugfix: Support nullable FKs with `SlugRelatedField`.
-* Bugfix: Don't call custom validation methods if the field has an error.
-
-**Note**: If the primary authentication class is `TokenAuthentication` or `BasicAuthentication`, a view will now correctly return 401 responses to unauthenticated access, with an appropriate `WWW-Authenticate` header, instead of 403 responses.
-
-### 2.1.16
-
-**Date**: 14th Jan 2013
-
-* Deprecate `django.utils.simplejson` in favor of Python 2.6's built-in json module.
-* Bugfix: `auto_now`, `auto_now_add` and other `editable=False` fields now default to read-only.
-* Bugfix: PK fields now only default to read-only if they are an AutoField or if `editable=False`.
-* Bugfix: Validation errors instead of exceptions when serializers receive incorrect types.
-* Bugfix: Validation errors instead of exceptions when related fields receive incorrect types.
-* Bugfix: Handle ObjectDoesNotExist exception when serializing null reverse one-to-one
-
-**Note**: Prior to 2.1.16, The Decimals would render in JSON using floating point if `simplejson` was installed, but otherwise render using string notation. Now that use of `simplejson` has been deprecated, Decimals will consistently render using string notation. See [#582] for more details.
-
-### 2.1.15
-
-**Date**: 3rd Jan 2013
-
-* Added `PATCH` support.
-* Added `RetrieveUpdateAPIView`.
-* Remove unused internal `save_m2m` flag on `ModelSerializer.save()`.
-* Tweak behavior of hyperlinked fields with an explicit format suffix.
-* Relation changes are now persisted in `.save()` instead of in `.restore_object()`.
-* Bugfix: Fix issue with FileField raising exception instead of validation error when files=None.
-* Bugfix: Partial updates should not set default values if field is not included.
-
-### 2.1.14
-
-**Date**: 31st Dec 2012
-
-* Bugfix: ModelSerializers now include reverse FK fields on creation.
-* Bugfix: Model fields with `blank=True` are now `required=False` by default.
-* Bugfix: Nested serializers now support nullable relationships.
-
-**Note**: From 2.1.14 onwards, relational fields move out of the `fields.py` module and into the new `relations.py` module, in order to separate them from regular data type fields, such as `CharField` and `IntegerField`.
-
-This change will not affect user code, so long as it's following the recommended import style of `from rest_framework import serializers` and referring to fields using the style `serializers.PrimaryKeyRelatedField`.
-
-
-### 2.1.13
-
-**Date**: 28th Dec 2012
-
-* Support configurable `STATICFILES_STORAGE` storage.
-* Bugfix: Related fields now respect the required flag, and may be required=False.
-
-### 2.1.12
-
-**Date**: 21st Dec 2012
-
-* Bugfix: Fix bug that could occur using ChoiceField.
-* Bugfix: Fix exception in browsable API on DELETE.
-* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg.
-
-### 2.1.11
-
-**Date**: 17th Dec 2012
-
-* Bugfix: Fix issue with M2M fields in browsable API.
-
-### 2.1.10
-
-**Date**: 17th Dec 2012
-
-* Bugfix: Ensure read-only fields don't have model validation applied.
-* Bugfix: Fix hyperlinked fields in paginated results.
-
-### 2.1.9
-
-**Date**: 11th Dec 2012
-
-* Bugfix: Fix broken nested serialization.
-* Bugfix: Fix `Meta.fields` only working as tuple not as list.
-* Bugfix: Edge case if unnecessarily specifying `required=False` on read only field.
-
-### 2.1.8
-
-**Date**: 8th Dec 2012
-
-* Fix for creating nullable Foreign Keys with `''` as well as `None`.
-* Added `null=<bool>` related field option.
-
-### 2.1.7
-
-**Date**: 7th Dec 2012
-
-* Serializers now properly support nullable Foreign Keys.
-* Serializer validation now includes model field validation, such as uniqueness constraints.
-* Support 'true' and 'false' string values for BooleanField.
-* Added pickle support for serialized data.
-* Support `source='dotted.notation'` style for nested serializers.
-* Make `Request.user` settable.
-* Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`.
-
-### 2.1.6
-
-**Date**: 23rd Nov 2012
-
-* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
-
-### 2.1.5
-
-**Date**: 23rd Nov 2012
-
-* Bugfix: Fix DjangoModelPermissions.
-
-### 2.1.4
-
-**Date**: 22nd Nov 2012
-
-* Support for partial updates with serializers.
-* Added `RegexField`.
-* Added `SerializerMethodField`.
-* Serializer performance improvements.
-* Added `obtain_token_view` to get tokens when using `TokenAuthentication`.
-* Bugfix: Django 1.5 configurable user support for `TokenAuthentication`.
-
-### 2.1.3
-
-**Date**: 16th Nov 2012
-
-* Added `FileField` and `ImageField`. For use with `MultiPartParser`.
-* Added `URLField` and `SlugField`.
-* Support for `read_only_fields` on `ModelSerializer` classes.
-* Support for clients overriding the pagination page sizes. Use the `PAGINATE_BY_PARAM` setting or set the `paginate_by_param` attribute on a generic view.
-* 201 Responses now return a 'Location' header.
-* Bugfix: Serializer fields now respect `max_length`.
-
-### 2.1.2
-
-**Date**: 9th Nov 2012
-
-* **Filtering support.**
-* Bugfix: Support creation of objects with reverse M2M relations.
-
-### 2.1.1
-
-**Date**: 7th Nov 2012
-
-* Support use of HTML exception templates. Eg. `403.html`
-* Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments.
-* Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs.
-* Bugfix: Make textareas same width as other fields in browsable API.
-* Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization.
-
-### 2.1.0
-
-**Date**: 5th Nov 2012
-
-* **Serializer `instance` and `data` keyword args have their position swapped.**
-* `queryset` argument is now optional on writable model fields.
-* Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments.
-* Support Django's cache framework.
-* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
-* Bugfix: Support choice field in Browsable API.
-* Bugfix: Related fields with `read_only=True` do not require a `queryset` argument.
-
-**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0.
-
----
-
-## 2.0.x series
-
-### 2.0.2
-
-**Date**: 2nd Nov 2012
-
-* Fix issues with pk related fields in the browsable API.
-
-### 2.0.1
-
-**Date**: 1st Nov 2012
-
-* Add support for relational fields in the browsable API.
-* Added SlugRelatedField and ManySlugRelatedField.
-* If PUT creates an instance return '201 Created', instead of '200 OK'.
-
-### 2.0.0
-
-**Date**: 30th Oct 2012
-
-* **Fix all of the things.** (Well, almost.)
-* For more information please see the [2.0 announcement][announcement].
-
----
-
-## 0.4.x series
-
-### 0.4.0
-
-* Supports Django 1.5.
-* Fixes issues with 'HEAD' method.
-* Allow views to specify template used by TemplateRenderer
-* More consistent error responses
-* Some serializer fixes
-* Fix internet explorer ajax behavior
-* Minor xml and yaml fixes
-* Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
-* Sensible absolute URL generation, not using hacky set_script_prefix
-
----
-
-## 0.3.x series
-
-### 0.3.3
-
-* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
-* Use `staticfiles` for css files.
- - Easier to override. Won't conflict with customized admin styles (e.g. grappelli)
-* Templates are now nicely namespaced.
- - Allows easier overriding.
-* Drop implied 'pk' filter if last arg in urlconf is unnamed.
- - Too magical. Explicit is better than implicit.
-* Saner template variable auto-escaping.
-* Tidier setup.py
-* Updated for URLObject 2.0
-* Bugfixes:
- - Bug with PerUserThrottling when user contains unicode chars.
-
-### 0.3.2
-
-* Bugfixes:
- * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
- * serialize_model method in serializer.py may cause wrong value (#73)
- * Fix Error when clicking OPTIONS button (#146)
- * And many other fixes
-* Remove short status codes
- - Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
-* get_name, get_description become methods on the view - makes them overridable.
-* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
-
-### 0.3.1
-
-* [not documented]
-
-### 0.3.0
-
-* JSONP Support
-* Bugfixes, including support for latest markdown release
-
----
-
-## 0.2.x series
-
-### 0.2.4
-
-* Fix broken IsAdminUser permission.
-* OPTIONS support.
-* XMLParser.
-* Drop mentions of Blog, BitBucket.
-
-### 0.2.3
-
-* Fix some throttling bugs.
-* ``X-Throttle`` header on throttling.
-* Support for nesting resources on related models.
-
-### 0.2.2
-
-* Throttling support complete.
-
-### 0.2.1
-
-* Couple of simple bugfixes over 0.2.0
-
-### 0.2.0
-
-* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
- The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
-
-* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
-
-* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
- Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
- Use ``self.user`` inside a view to access the authenticated user.
-
-* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
- The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
- Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
-
-* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
-
-* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
-
-* ``ResponseException`` becomes ``ErrorResponse``.
-
-* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
- You can reuse these mixin classes individually without using the ``View`` class.
+## 3.0.x series
+
+### 3.1.0
+
+**Date**: [5th March 2015][3.1.0-milestone].
+
+For full details see the [3.1 release announcement](3.1-announcement.md).
+
+### 3.0.5
+
+**Date**: [10th February 2015][3.0.5-milestone].
+
+* Fix a bug where `_closable_objects` breaks pickling. ([#1850][gh1850], [#2492][gh2492])
+* Allow non-standard `User` models with `Throttling`. ([#2524][gh2524])
+* Support custom `User.db_table` in TokenAuthentication migration. ([#2479][gh2479])
+* Fix misleading `AttributeError` tracebacks on `Request` objects. ([#2530][gh2530], [#2108][gh2108])
+* `ManyRelatedField.get_value` clearing field on partial update. ([#2475][gh2475])
+* Removed '.model' shortcut from code. ([#2486][gh2486])
+* Fix `detail_route` and `list_route` mutable argument. ([#2518][gh2518])
+* Prefetching the user object when getting the token in `TokenAuthentication`. ([#2519][gh2519])
+
+### 3.0.4
+
+**Date**: [28th January 2015][3.0.4-milestone].
+
+* Django 1.8a1 support. ([#2425][gh2425], [#2446][gh2446], [#2441][gh2441])
+* Add `DictField` and support Django 1.8 `HStoreField`. ([#2451][gh2451], [#2106][gh2106])
+* Add `UUIDField` and support Django 1.8 `UUIDField`. ([#2448][gh2448], [#2433][gh2433], [#2432][gh2432])
+* `BaseRenderer.render` now raises `NotImplementedError`. ([#2434][gh2434])
+* Fix timedelta JSON serialization on Python 2.6. ([#2430][gh2430])
+* `ResultDict` and `ResultList` now appear as standard dict/list. ([#2421][gh2421])
+* Fix visible `HiddenField` in the HTML form of the web browsable API page. ([#2410][gh2410])
+* Use `OrderedDict` for `RelatedField.choices`. ([#2408][gh2408])
+* Fix ident format when using `HTTP_X_FORWARDED_FOR`. ([#2401][gh2401])
+* Fix invalid key with memcached while using throttling. ([#2400][gh2400])
+* Fix `FileUploadParser` with version 3.x. ([#2399][gh2399])
+* Fix the serializer inheritance. ([#2388][gh2388])
+* Fix caching issues with `ReturnDict`. ([#2360][gh2360])
+
+### 3.0.3
+
+**Date**: [8th January 2015][3.0.3-milestone].
+
+* Fix `MinValueValidator` on `models.DateField`. ([#2369][gh2369])
+* Fix serializer missing context when pagination is used. ([#2355][gh2355])
+* Namespaced router URLs are now supported by the `DefaultRouter`. ([#2351][gh2351])
+* `required=False` allows omission of value for output. ([#2342][gh2342])
+* Use textarea input for `models.TextField`. ([#2340][gh2340])
+* Use custom `ListSerializer` for pagination if required. ([#2331][gh2331], [#2327][gh2327])
+* Better behavior with null and '' for blank HTML fields. ([#2330][gh2330])
+* Ensure fields in `exclude` are model fields. ([#2319][gh2319])
+* Fix `IntegerField` and `max_length` argument incompatibility. ([#2317][gh2317])
+* Fix the YAML encoder for 3.0 serializers. ([#2315][gh2315], [#2283][gh2283])
+* Fix the behavior of empty HTML fields. ([#2311][gh2311], [#1101][gh1101])
+* Fix Metaclass attribute depth ignoring fields attribute. ([#2287][gh2287])
+* Fix `format_suffix_patterns` to work with Django's `i18n_patterns`. ([#2278][gh2278])
+* Ability to customize router URLs for custom actions, using `url_path`. ([#2010][gh2010])
+* Don't install Django REST Framework as egg. ([#2386][gh2386])
+
+### 3.0.2
+
+**Date**: [17th December 2014][3.0.2-milestone].
+
+* Ensure `request.user` is made available to response middleware. ([#2155][gh2155])
+* `Client.logout()` also cancels any existing `force_authenticate`. ([#2218][gh2218], [#2259][gh2259])
+* Extra assertions and better checks to preventing incorrect serializer API use. ([#2228][gh2228], [#2234][gh2234], [#2262][gh2262], [#2263][gh2263], [#2266][gh2266], [#2267][gh2267], [#2289][gh2289], [#2291][gh2291])
+* Fixed `min_length` message for `CharField`. ([#2255][gh2255])
+* Fix `UnicodeDecodeError`, which can occur on serializer `repr`. ([#2270][gh2270], [#2279][gh2279])
+* Fix empty HTML values when a default is provided. ([#2280][gh2280], [#2294][gh2294])
+* Fix `SlugRelatedField` raising `UnicodeEncodeError` when used as a multiple choice input. ([#2290][gh2290])
+
+### 3.0.1
+
+**Date**: [11th December 2014][3.0.1-milestone].
+
+* More helpful error message when the default Serializer `create()` fails. ([#2013][gh2013])
+* Raise error when attempting to save serializer if data is not valid. ([#2098][gh2098])
+* Fix `FileUploadParser` breaks with empty file names and multiple upload handlers. ([#2109][gh2109])
+* Improve `BindingDict` to support standard dict-functions. ([#2135][gh2135], [#2163][gh2163])
+* Add `validate()` to `ListSerializer`. ([#2168][gh2168], [#2225][gh2225], [#2232][gh2232])
+* Fix JSONP renderer failing to escape some characters. ([#2169][gh2169], [#2195][gh2195])
+* Add missing default style for `FileField`. ([#2172][gh2172])
+* Actions are required when calling `ViewSet.as_view()`. ([#2175][gh2175])
+* Add `allow_blank` to `ChoiceField`. ([#2184][gh2184], [#2239][gh2239])
+* Cosmetic fixes in the HTML renderer. ([#2187][gh2187])
+* Raise error if `fields` on serializer is not a list of strings. ([#2193][gh2193], [#2213][gh2213])
+* Improve checks for nested creates and updates. ([#2194][gh2194], [#2196][gh2196])
+* `validated_attrs` argument renamed to `validated_data` in `Serializer` `create()`/`update()`. ([#2197][gh2197])
+* Remove deprecated code to reflect the dropped Django versions. ([#2200][gh2200])
+* Better serializer errors for nested writes. ([#2202][gh2202], [#2215][gh2215])
+* Fix pagination and custom permissions incompatibility. ([#2205][gh2205])
+* Raise error if `fields` on serializer is not a list of strings. ([#2213][gh2213])
+* Add missing translation markers for relational fields. ([#2231][gh2231])
+* Improve field lookup behavior for dicts/mappings. ([#2244][gh2244], [#2243][gh2243])
+* Optimized hyperlinked PK. ([#2242][gh2242])
+
+### 3.0.0
+
+**Date**: 1st December 2014
+
+For full details see the [3.0 release announcement](3.0-announcement.md).
---
-## 0.1.x series
-
-### 0.1.1
-
-* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
-
-### 0.1.0
-
-* Initial release.
+For older release notes, [please see the version 2.x documentation](old-release-notes).
[cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html
[deprecation-policy]: #deprecation-policy
[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy
[defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html
-[2.2-announcement]: 2.2-announcement.md
-[2.3-announcement]: 2.3-announcement.md
[743]: https://github.com/tomchristie/django-rest-framework/pull/743
[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag
[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
-[announcement]: rest-framework-2-announcement.md
-[#582]: https://github.com/tomchristie/django-rest-framework/issues/582
+[ticket-582]: https://github.com/tomchristie/django-rest-framework/issues/582
+[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
+[old-release-notes]: http://tomchristie.github.io/rest-framework-2-docs/topics/release-notes#24x-series
+
+[3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22
+[3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22
+[3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22
+[3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22
+[3.0.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22
+
+<!-- 3.0.1 -->
+[gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013
+[gh2098]: https://github.com/tomchristie/django-rest-framework/issues/2098
+[gh2109]: https://github.com/tomchristie/django-rest-framework/issues/2109
+[gh2135]: https://github.com/tomchristie/django-rest-framework/issues/2135
+[gh2163]: https://github.com/tomchristie/django-rest-framework/issues/2163
+[gh2168]: https://github.com/tomchristie/django-rest-framework/issues/2168
+[gh2169]: https://github.com/tomchristie/django-rest-framework/issues/2169
+[gh2172]: https://github.com/tomchristie/django-rest-framework/issues/2172
+[gh2175]: https://github.com/tomchristie/django-rest-framework/issues/2175
+[gh2184]: https://github.com/tomchristie/django-rest-framework/issues/2184
+[gh2187]: https://github.com/tomchristie/django-rest-framework/issues/2187
+[gh2193]: https://github.com/tomchristie/django-rest-framework/issues/2193
+[gh2194]: https://github.com/tomchristie/django-rest-framework/issues/2194
+[gh2195]: https://github.com/tomchristie/django-rest-framework/issues/2195
+[gh2196]: https://github.com/tomchristie/django-rest-framework/issues/2196
+[gh2197]: https://github.com/tomchristie/django-rest-framework/issues/2197
+[gh2200]: https://github.com/tomchristie/django-rest-framework/issues/2200
+[gh2202]: https://github.com/tomchristie/django-rest-framework/issues/2202
+[gh2205]: https://github.com/tomchristie/django-rest-framework/issues/2205
+[gh2213]: https://github.com/tomchristie/django-rest-framework/issues/2213
+[gh2213]: https://github.com/tomchristie/django-rest-framework/issues/2213
+[gh2215]: https://github.com/tomchristie/django-rest-framework/issues/2215
+[gh2225]: https://github.com/tomchristie/django-rest-framework/issues/2225
+[gh2231]: https://github.com/tomchristie/django-rest-framework/issues/2231
+[gh2232]: https://github.com/tomchristie/django-rest-framework/issues/2232
+[gh2239]: https://github.com/tomchristie/django-rest-framework/issues/2239
+[gh2242]: https://github.com/tomchristie/django-rest-framework/issues/2242
+[gh2243]: https://github.com/tomchristie/django-rest-framework/issues/2243
+[gh2244]: https://github.com/tomchristie/django-rest-framework/issues/2244
+<!-- 3.0.2 -->
+[gh2155]: https://github.com/tomchristie/django-rest-framework/issues/2155
+[gh2218]: https://github.com/tomchristie/django-rest-framework/issues/2218
+[gh2228]: https://github.com/tomchristie/django-rest-framework/issues/2228
+[gh2234]: https://github.com/tomchristie/django-rest-framework/issues/2234
+[gh2255]: https://github.com/tomchristie/django-rest-framework/issues/2255
+[gh2259]: https://github.com/tomchristie/django-rest-framework/issues/2259
+[gh2262]: https://github.com/tomchristie/django-rest-framework/issues/2262
+[gh2263]: https://github.com/tomchristie/django-rest-framework/issues/2263
+[gh2266]: https://github.com/tomchristie/django-rest-framework/issues/2266
+[gh2267]: https://github.com/tomchristie/django-rest-framework/issues/2267
+[gh2270]: https://github.com/tomchristie/django-rest-framework/issues/2270
+[gh2279]: https://github.com/tomchristie/django-rest-framework/issues/2279
+[gh2280]: https://github.com/tomchristie/django-rest-framework/issues/2280
+[gh2289]: https://github.com/tomchristie/django-rest-framework/issues/2289
+[gh2290]: https://github.com/tomchristie/django-rest-framework/issues/2290
+[gh2291]: https://github.com/tomchristie/django-rest-framework/issues/2291
+[gh2294]: https://github.com/tomchristie/django-rest-framework/issues/2294
+<!-- 3.0.3 -->
+[gh1101]: https://github.com/tomchristie/django-rest-framework/issues/1101
+[gh2010]: https://github.com/tomchristie/django-rest-framework/issues/2010
+[gh2278]: https://github.com/tomchristie/django-rest-framework/issues/2278
+[gh2283]: https://github.com/tomchristie/django-rest-framework/issues/2283
+[gh2287]: https://github.com/tomchristie/django-rest-framework/issues/2287
+[gh2311]: https://github.com/tomchristie/django-rest-framework/issues/2311
+[gh2315]: https://github.com/tomchristie/django-rest-framework/issues/2315
+[gh2317]: https://github.com/tomchristie/django-rest-framework/issues/2317
+[gh2319]: https://github.com/tomchristie/django-rest-framework/issues/2319
+[gh2327]: https://github.com/tomchristie/django-rest-framework/issues/2327
+[gh2330]: https://github.com/tomchristie/django-rest-framework/issues/2330
+[gh2331]: https://github.com/tomchristie/django-rest-framework/issues/2331
+[gh2340]: https://github.com/tomchristie/django-rest-framework/issues/2340
+[gh2342]: https://github.com/tomchristie/django-rest-framework/issues/2342
+[gh2351]: https://github.com/tomchristie/django-rest-framework/issues/2351
+[gh2355]: https://github.com/tomchristie/django-rest-framework/issues/2355
+[gh2369]: https://github.com/tomchristie/django-rest-framework/issues/2369
+[gh2386]: https://github.com/tomchristie/django-rest-framework/issues/2386
+<!-- 3.0.4 -->
+[gh2425]: https://github.com/tomchristie/django-rest-framework/issues/2425
+[gh2446]: https://github.com/tomchristie/django-rest-framework/issues/2446
+[gh2441]: https://github.com/tomchristie/django-rest-framework/issues/2441
+[gh2451]: https://github.com/tomchristie/django-rest-framework/issues/2451
+[gh2106]: https://github.com/tomchristie/django-rest-framework/issues/2106
+[gh2448]: https://github.com/tomchristie/django-rest-framework/issues/2448
+[gh2433]: https://github.com/tomchristie/django-rest-framework/issues/2433
+[gh2432]: https://github.com/tomchristie/django-rest-framework/issues/2432
+[gh2434]: https://github.com/tomchristie/django-rest-framework/issues/2434
+[gh2430]: https://github.com/tomchristie/django-rest-framework/issues/2430
+[gh2421]: https://github.com/tomchristie/django-rest-framework/issues/2421
+[gh2410]: https://github.com/tomchristie/django-rest-framework/issues/2410
+[gh2408]: https://github.com/tomchristie/django-rest-framework/issues/2408
+[gh2401]: https://github.com/tomchristie/django-rest-framework/issues/2401
+[gh2400]: https://github.com/tomchristie/django-rest-framework/issues/2400
+[gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399
+[gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388
+[gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360
+<!-- 3.0.5 -->
+[gh1850]: https://github.com/tomchristie/django-rest-framework/issues/1850
+[gh2108]: https://github.com/tomchristie/django-rest-framework/issues/2108
+[gh2475]: https://github.com/tomchristie/django-rest-framework/issues/2475
+[gh2479]: https://github.com/tomchristie/django-rest-framework/issues/2479
+[gh2486]: https://github.com/tomchristie/django-rest-framework/issues/2486
+[gh2492]: https://github.com/tomchristie/django-rest-framework/issues/2492
+[gh2518]: https://github.com/tomchristie/django-rest-framework/issues/2518
+[gh2519]: https://github.com/tomchristie/django-rest-framework/issues/2519
+[gh2524]: https://github.com/tomchristie/django-rest-framework/issues/2524
+[gh2530]: https://github.com/tomchristie/django-rest-framework/issues/2530
diff --git a/docs/topics/rest-framework-2-announcement.md b/docs/topics/rest-framework-2-announcement.md
index f1060d90..ed41bb48 100644
--- a/docs/topics/rest-framework-2-announcement.md
+++ b/docs/topics/rest-framework-2-announcement.md
@@ -1,6 +1,4 @@
-# Django REST framework 2
-
-What it is, and why you should care.
+# Django REST framework 2.0
> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result.
>
@@ -8,7 +6,7 @@ What it is, and why you should care.
---
-**Announcement:** REST framework 2 released - Tue 30th Oct 2012
+**Announcement:** REST framework 2 released - Tue 30th Oct 2012
---
@@ -37,7 +35,7 @@ REST framework 2 includes a totally re-worked serialization engine, that was ini
* A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API.
* Structural concerns are decoupled from encoding concerns.
* Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms.
-* Validation that can be mapped to obvious and comprehensive error responses.
+* Validation that can be mapped to obvious and comprehensive error responses.
* Serializers that support both nested, flat, and partially-nested representations.
* Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations.
diff --git a/docs/topics/rest-hypermedia-hateoas.md b/docs/topics/rest-hypermedia-hateoas.md
index 3d700488..7e6d2408 100644
--- a/docs/topics/rest-hypermedia-hateoas.md
+++ b/docs/topics/rest-hypermedia-hateoas.md
@@ -1,19 +1,19 @@
# REST, Hypermedia & HATEOAS
-> You keep using that word "REST". I do not think it means what you think it means.
+> You keep using that word "REST". I do not think it means what you think it means.
>
> &mdash; Mike Amundsen, [REST fest 2012 keynote][cite].
-First off, the disclaimer. The name "Django REST framework" was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
+First off, the disclaimer. The name "Django REST framework" was decided back in early 2011 and was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
-If you are serious about designing a Hypermedia APIs, you should look to resources outside of this documentation to help inform your design choices.
+If you are serious about designing a Hypermedia API, you should look to resources outside of this documentation to help inform your design choices.
The following fall into the "required reading" category.
* Roy Fielding's dissertation - [Architectural Styles and
the Design of Network-based Software Architectures][dissertation].
* Roy Fielding's "[REST APIs must be hypertext-driven][hypertext-driven]" blog post.
-* Leonard Richardson & Sam Ruby's [RESTful Web Services][restful-web-services].
+* Leonard Richardson & Mike Amundsen's [RESTful Web APIs][restful-web-apis].
* Mike Amundsen's [Building Hypermedia APIs with HTML5 and Node][building-hypermedia-apis].
* Steve Klabnik's [Designing Hypermedia APIs][designing-hypermedia-apis].
* The [Richardson Maturity Model][maturitymodel].
@@ -32,12 +32,12 @@ REST framework also includes [serialization] and [parser]/[renderer] components
## What REST framework doesn't provide.
-What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection], [JSON API][json-api] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
+What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection], [JSON API][json-api] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
[cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
-[restful-web-services]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
+[restful-web-apis]: http://restfulwebapis.org/
[building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
[restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md
new file mode 100644
index 00000000..2f46e1fc
--- /dev/null
+++ b/docs/topics/third-party-resources.md
@@ -0,0 +1,328 @@
+# Third Party Resources
+
+> Software ecosystems […] establish a community that further accelerates the sharing of knowledge, content, issues, expertise and skills.
+>
+> &mdash; [Jan Bosch][cite].
+
+## About Third Party Packages
+
+Third Party Packages allow developers to share code that extends the functionality of Django REST framework, in order to support additional use-cases.
+
+We **support**, **encourage** and **strongly favor** the creation of Third Party Packages to encapsulate new behavior rather than adding additional functionality directly to Django REST Framework.
+
+We aim to make creating third party packages as easy as possible, whilst keeping a **simple** and **well maintained** core API. By promoting third party packages we ensure that the responsibility for a package remains with its author. If a package proves suitably popular it can always be considered for inclusion into the core REST framework.
+
+If you have an idea for a new feature please consider how it may be packaged as a Third Party Package. We're always happy to discuss ideas on the [Mailing List][discussion-group].
+
+## How to create a Third Party Package
+
+### Creating your package
+
+You can use [this cookiecutter template][cookiecutter] for creating reusable Django REST Framework packages quickly. Cookiecutter creates projects from project templates. While optional, this cookiecutter template includes best practices from Django REST framework and other packages, as well as a Travis CI configuration, Tox configuration, and a sane setup.py for easy PyPI registration/distribution.
+
+Note: Let us know if you have an alternate cookiecuter package so we can also link to it.
+
+#### Running the initial cookiecutter command
+
+To run the initial cookiecutter command, you'll first need to install the Python `cookiecutter` package.
+
+ $ pip install cookiecutter
+
+Once `cookiecutter` is installed just run the following to create a new project.
+
+ $ cookiecutter gh:jpadilla/cookiecutter-django-rest-framework
+
+You'll be prompted for some questions, answer them, then it'll create your Python package in the current working directory based on those values.
+
+ full_name (default is "Your full name here")? Johnny Appleseed
+ email (default is "you@example.com")? jappleseed@example.com
+ github_username (default is "yourname")? jappleseed
+ pypi_project_name (default is "dj-package")? djangorestframework-custom-auth
+ repo_name (default is "dj-package")? django-rest-framework-custom-auth
+ app_name (default is "djpackage")? custom_auth
+ project_short_description (default is "Your project description goes here")?
+ year (default is "2014")?
+ version (default is "0.1.0")?
+
+#### Getting it onto GitHub
+
+To put your project up on GitHub, you'll need a repository for it to live in. You can create a new repository [here][new-repo]. If you need help, check out the [Create A Repo][create-a-repo] article on GitHub.
+
+
+#### Adding to Travis CI
+
+We recommend using [Travis CI][travis-ci], a hosted continuous integration service which integrates well with GitHub and is free for public repositories.
+
+To get started with Travis CI, [sign in][travis-ci] with your GitHub account. Once you're signed in, go to your [profile page][travis-profile] and enable the service hook for the repository you want.
+
+If you use the cookiecutter template, your project will already contain a `.travis.yml` file which Travis CI will use to build your project and run tests. By default, builds are triggered everytime you push to your repository or create Pull Request.
+
+#### Uploading to PyPI
+
+Once you've got at least a prototype working and tests running, you should publish it on PyPI to allow others to install it via `pip`.
+
+You must [register][pypi-register] an account before publishing to PyPI.
+
+To register your package on PyPI run the following command.
+
+ $ python setup.py register
+
+If this is the first time publishing to PyPI, you'll be prompted to login.
+
+Note: Before publishing you'll need to make sure you have the latest pip that supports `wheel` as well as install the `wheel` package.
+
+ $ pip install --upgrade pip
+ $ pip install wheel
+
+After this, every time you want to release a new version on PyPI just run the following command.
+
+ $ python setup.py publish
+ You probably want to also tag the version now:
+ git tag -a {0} -m 'version 0.1.0'
+ git push --tags
+
+After releasing a new version to PyPI, it's always a good idea to tag the version and make available as a GitHub Release.
+
+We recommend to follow [Semantic Versioning][semver] for your package's versions.
+
+### Development
+
+#### Version requirements
+
+The cookiecutter template assumes a set of supported versions will be provided for Python and Django. Make sure you correctly update your requirements, docs, `tox.ini`, `.travis.yml`, and `setup.py` to match the set of versions you wish to support.
+
+#### Tests
+
+The cookiecutter template includes a `runtests.py` which uses the `pytest` package as a test runner.
+
+Before running, you'll need to install a couple test requirements.
+
+ $ pip install -r requirements.txt
+
+Once requirements installed, you can run `runtests.py`.
+
+ $ ./runtests.py
+
+Run using a more concise output style.
+
+ $ ./runtests.py -q
+
+Run the tests using a more concise output style, no coverage, no flake8.
+
+ $ ./runtests.py --fast
+
+Don't run the flake8 code linting.
+
+ $ ./runtests.py --nolint
+
+Only run the flake8 code linting, don't run the tests.
+
+ $ ./runtests.py --lintonly
+
+Run the tests for a given test case.
+
+ $ ./runtests.py MyTestCase
+
+Run the tests for a given test method.
+
+ $ ./runtests.py MyTestCase.test_this_method
+
+Shorter form to run the tests for a given test method.
+
+ $ ./runtests.py test_this_method
+
+To run your tests against multiple versions of Python as different versions of requirements such as Django we recommend using `tox`. [Tox][tox-docs] is a generic virtualenv management and test command line tool.
+
+First, install `tox` globally.
+
+ $ pip install tox
+
+To run `tox`, just simply run:
+
+ $ tox
+
+To run a particular `tox` environment:
+
+ $ tox -e envlist
+
+`envlist` is a comma-separated value to that specifies the environments to run tests against. To view a list of all possible test environments, run:
+
+ $ tox -l
+
+#### Version compatibility
+
+Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into a `compat.py` module, and should provide a single common interface that the rest of the codebase can use.
+
+Check out Django REST framework's [compat.py][drf-compat] for an example.
+
+### Once your package is available
+
+Once your package is decently documented and available on PyPI, you might want share it with others that might find it useful.
+
+#### Adding to the Django REST framework grid
+
+We suggest adding your package to the [REST Framework][rest-framework-grid] grid on Django Packages.
+
+#### Adding to the Django REST framework docs
+
+Create a [Pull Request][drf-create-pr] or [Issue][drf-create-issue] on GitHub, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Resources][third-party-resources] section.
+
+#### Announce on the discussion group.
+
+You can also let others know about your package through the [discussion group][discussion-group].
+
+## Existing Third Party Packages
+
+Django REST Framework has a growing community of developers, packages, and resources.
+
+Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages][rest-framework-grid].
+
+To submit new content, [open an issue][drf-create-issue] or [create a pull request][drf-create-pr].
+
+### Authentication
+
+* [djangorestframework-digestauth][djangorestframework-digestauth] - Provides Digest Access Authentication support.
+* [django-oauth-toolkit][django-oauth-toolkit] - Provides OAuth 2.0 support.
+* [doac][doac] - Provides OAuth 2.0 support.
+* [djangorestframework-jwt][djangorestframework-jwt] - Provides JSON Web Token Authentication support.
+* [hawkrest][hawkrest] - Provides Hawk HTTP Authorization.
+* [djangorestframework-httpsignature][djangorestframework-httpsignature] - Provides an easy to use HTTP Signature Authentication mechanism.
+* [djoser][djoser] - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
+* [django-rest-auth][django-rest-auth] - Provides a set of REST API endpoints for registration, authentication (including social media authentication), password reset, retrieve and update user details, etc.
+
+### Permissions
+
+* [drf-any-permissions][drf-any-permissions] - Provides alternative permission handling.
+* [djangorestframework-composed-permissions][djangorestframework-composed-permissions] - Provides a simple way to define complex permissions.
+* [rest_condition][rest-condition] - Another extension for building complex permissions in a simple and convenient way.
+
+### Serializers
+
+* [django-rest-framework-mongoengine][django-rest-framework-mongoengine] - Serializer class that supports using MongoDB as the storage layer for Django REST framework.
+* [djangorestframework-gis][djangorestframework-gis] - Geographic add-ons
+* [djangorestframework-hstore][djangorestframework-hstore] - Serializer class to support django-hstore DictionaryField model field and its schema-mode feature.
+
+### Serializer fields
+
+* [drf-compound-fields][drf-compound-fields] - Provides "compound" serializer fields, such as lists of simple values.
+* [django-extra-fields][django-extra-fields] - Provides extra serializer fields.
+
+### Views
+
+* [djangorestframework-bulk][djangorestframework-bulk] - Implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
+
+### Routers
+
+* [drf-nested-routers][drf-nested-routers] - Provides routers and relationship fields for working with nested resources.
+* [wq.db.rest][wq.db.rest] - Provides an admin-style model registration API with reasonable default URLs and viewsets.
+
+### Parsers
+
+* [djangorestframework-msgpack][djangorestframework-msgpack] - Provides MessagePack renderer and parser support.
+* [djangorestframework-camel-case][djangorestframework-camel-case] - Provides camel case JSON renderers and parsers.
+
+### Renderers
+
+* [djangorestframework-csv][djangorestframework-csv] - Provides CSV renderer support.
+* [drf_ujson][drf_ujson] - Implements JSON rendering using the UJSON package.
+* [rest-pandas][rest-pandas] - Pandas DataFrame-powered renderers including Excel, CSV, and SVG formats.
+
+### Filtering
+
+* [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters.
+
+### Misc
+
+* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
+* [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI.
+* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
+* [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok
+* [drf-extensions][drf-extensions] - A collection of custom extensions
+* [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js
+
+## Other Resources
+
+### Tutorials
+
+* [Beginner's Guide to the Django Rest Framework][beginners-guide-to-the-django-rest-framework]
+* [Getting Started with Django Rest Framework and AngularJS][getting-started-with-django-rest-framework-and-angularjs]
+* [End to end web app with Django-Rest-Framework & AngularJS][end-to-end-web-app-with-django-rest-framework-angularjs]
+* [Start Your API - django-rest-framework part 1][start-your-api-django-rest-framework-part-1]
+* [Permissions & Authentication - django-rest-framework part 2][permissions-authentication-django-rest-framework-part-2]
+* [ViewSets and Routers - django-rest-framework part 3][viewsets-and-routers-django-rest-framework-part-3]
+* [Django Rest Framework User Endpoint][django-rest-framework-user-endpoint]
+* [Check credentials using Django Rest Framework][check-credentials-using-django-rest-framework]
+
+### Videos
+
+* [Ember and Django Part 1 (Video)][ember-and-django-part 1-video]
+* [Django Rest Framework Part 1 (Video)][django-rest-framework-part-1-video]
+* [Pyowa July 2013 - Django Rest Framework (Video)][pyowa-july-2013-django-rest-framework-video]
+* [django-rest-framework and angularjs (Video)][django-rest-framework-and-angularjs-video]
+
+### Articles
+
+* [Web API performance: profiling Django REST framework][web-api-performance-profiling-django-rest-framework]
+* [API Development with Django and Django REST Framework][api-development-with-django-and-django-rest-framework]
+
+[cite]: http://www.software-ecosystems.com/Software_Ecosystems/Ecosystems.html
+[cookiecutter]: https://github.com/jpadilla/cookiecutter-django-rest-framework
+[new-repo]: https://github.com/new
+[create-a-repo]: https://help.github.com/articles/create-a-repo/
+[travis-ci]: https://travis-ci.org
+[travis-profile]: https://travis-ci.org/profile
+[pypi-register]: https://pypi.python.org/pypi?%3Aaction=register_form
+[semver]: http://semver.org/
+[tox-docs]: https://tox.readthedocs.org/en/latest/
+[drf-compat]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/compat.py
+[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
+[drf-create-pr]: https://github.com/tomchristie/django-rest-framework/compare
+[drf-create-issue]: https://github.com/tomchristie/django-rest-framework/issues/new
+[authentication]: ../api-guide/authentication.md
+[permissions]: ../api-guide/permissions.md
+[discussion-group]: https://groups.google.com/forum/#!forum/django-rest-framework
+[djangorestframework-digestauth]: https://github.com/juanriaza/django-rest-framework-digestauth
+[django-oauth-toolkit]: https://github.com/evonove/django-oauth-toolkit
+[doac]: https://github.com/Rediker-Software/doac
+[djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt
+[hawkrest]: https://github.com/kumar303/hawkrest
+[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature
+[djoser]: https://github.com/sunscrapers/djoser
+[drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions
+[djangorestframework-composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions
+[rest-condition]: https://github.com/caxap/rest_condition
+[django-rest-framework-mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine
+[djangorestframework-gis]: https://github.com/djangonauts/django-rest-framework-gis
+[djangorestframework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
+[drf-compound-fields]: https://github.com/estebistec/drf-compound-fields
+[django-extra-fields]: https://github.com/Hipo/drf-extra-fields
+[djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk
+[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
+[wq.db.rest]: http://wq.io/docs/about-rest
+[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
+[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
+[drf_ujson]: https://github.com/gizmag/drf-ujson-renderer
+[rest-pandas]: https://github.com/wq/django-rest-pandas
+[djangorestframework-chain]: https://github.com/philipn/django-rest-framework-chain
+[djangorestrelationalhyperlink]: https://github.com/fredkingham/django_rest_model_hyperlink_serializers_project
+[django-rest-swagger]: https://github.com/marcgibbons/django-rest-swagger
+[django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy
+[gaiarestframework]: https://github.com/AppsFuel/gaiarestframework
+[drf-extensions]: https://github.com/chibisov/drf-extensions
+[ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter
+[beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786
+[getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html
+[end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework
+[start-your-api-django-rest-framework-part-1]: https://godjango.com/41-start-your-api-django-rest-framework-part-1/
+[permissions-authentication-django-rest-framework-part-2]: https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/
+[viewsets-and-routers-django-rest-framework-part-3]: https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/
+[django-rest-framework-user-endpoint]: http://richardtier.com/2014/02/25/django-rest-framework-user-endpoint/
+[check-credentials-using-django-rest-framework]: http://richardtier.com/2014/03/06/110/
+[ember-and-django-part 1-video]: http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1
+[django-rest-framework-part-1-video]: http://www.neckbeardrepublic.com/screencasts/django-rest-framework-part-1
+[pyowa-july-2013-django-rest-framework-video]: http://www.youtube.com/watch?v=e1zrehvxpbo
+[django-rest-framework-and-angularjs-video]: http://www.youtube.com/watch?v=q8frbgtj020
+[web-api-performance-profiling-django-rest-framework]: http://dabapps.com/blog/api-performance-profiling-django-rest-framework/
+[api-development-with-django-and-django-rest-framework]: https://bnotions.com/api-development-with-django-and-django-rest-framework/
+[django-rest-auth]: https://github.com/Tivix/django-rest-auth/
diff --git a/docs/topics/writable-nested-serializers.md b/docs/topics/writable-nested-serializers.md
new file mode 100644
index 00000000..ed614bd2
--- /dev/null
+++ b/docs/topics/writable-nested-serializers.md
@@ -0,0 +1,47 @@
+> To save HTTP requests, it may be convenient to send related documents along with the request.
+>
+> &mdash; [JSON API specification for Ember Data][cite].
+
+# Writable nested serializers
+
+Although flat data structures serve to properly delineate between the individual entities in your service, there are cases where it may be more appropriate or convenient to use nested data structures.
+
+Nested data structures are easy enough to work with if they're read-only - simply nest your serializer classes and you're good to go. However, there are a few more subtleties to using writable nested serializers, due to the dependencies between the various model instances, and the need to save or delete multiple instances in a single action.
+
+## One-to-many data structures
+
+*Example of a **read-only** nested serializer. Nothing complex to worry about here.*
+
+ class ToDoItemSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ToDoItem
+ fields = ('text', 'is_completed')
+
+ class ToDoListSerializer(serializers.ModelSerializer):
+ items = ToDoItemSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = ToDoList
+ fields = ('title', 'items')
+
+Some example output from our serializer.
+
+ {
+ 'title': 'Leaving party preperations',
+ 'items': {
+ {'text': 'Compile playlist', 'is_completed': True},
+ {'text': 'Send invites', 'is_completed': False},
+ {'text': 'Clean house', 'is_completed': False}
+ }
+ }
+
+Let's take a look at updating our nested one-to-many data structure.
+
+### Validation errors
+
+### Adding and removing items
+
+### Making PATCH requests
+
+
+[cite]: http://jsonapi.org/format/#url-based-json-api
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 2b214d6a..ceb23a02 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -16,10 +16,8 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
- :::bash
- mkdir ~/env
- virtualenv ~/env/tutorial
- source ~/env/tutorial/bin/activate
+ virtualenv env
+ source env/bin/activate
Now that we're inside a virtualenv environment, we can install our package requirements.
@@ -42,20 +40,7 @@ Once that's done we can create an app that we'll use to create a simple Web API.
python manage.py startapp snippets
-The simplest way to get up and running will probably be to use an `sqlite3` database for the tutorial. Edit the `tutorial/settings.py` file, and set the default database `"ENGINE"` to `"sqlite3"`, and `"NAME"` to `"tmp.db"`.
-
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'tmp.db',
- 'USER': '',
- 'PASSWORD': '',
- 'HOST': '',
- 'PORT': '',
- }
- }
-
-We'll also need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`.
+We'll need to add our new `snippets` app and the `rest_framework` app to `INSTALLED_APPS`. Let's edit the `tutorial/settings.py` file:
INSTALLED_APPS = (
...
@@ -65,15 +50,15 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include('snippets.urls')),
- )
+ ]
Okay, we're ready to roll.
## Creating a model to work with
-For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets` app's `models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
+For the purposes of this tutorial we're going to start by creating a simple `Snippet` model that is used to store code snippets. Go ahead and edit the `snippets/models.py` file. Note: Good programming practices include comments. Although you will find them in our repository version of this tutorial code, we have omitted them here to focus on the code itself.
from django.db import models
from pygments.lexers import get_all_lexers
@@ -82,30 +67,27 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
-
-
+
+
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
- language = models.CharField(choices=LANGUAGE_CHOICES,
- default='python',
- max_length=100)
- style = models.CharField(choices=STYLE_CHOICES,
- default='friendly',
- max_length=100)
-
+ language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
+ style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
+
class Meta:
ordering = ('created',)
-Don't forget to sync the database for the first time.
+We'll also need to create an initial migration for our snippet model, and sync the database for the first time.
- python manage.py syncdb
+ python manage.py makemigrations snippets
+ python manage.py migrate
## Creating a Serializer class
-The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
+The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
from django.forms import widgets
from rest_framework import serializers
@@ -113,42 +95,38 @@ The first thing we need to get started on our Web API is provide a way of serial
class SnippetSerializer(serializers.Serializer):
- pk = serializers.Field() # Note: `Field` is an untyped read-only field.
- title = serializers.CharField(required=False,
- max_length=100)
- code = serializers.CharField(widget=widgets.Textarea,
- max_length=100000)
+ pk = serializers.IntegerField(read_only=True)
+ title = serializers.CharField(required=False, allow_blank=True, max_length=100)
+ code = serializers.CharField(style={'base_template': 'textarea.html'})
linenos = serializers.BooleanField(required=False)
- language = serializers.ChoiceField(choices=LANGUAGE_CHOICES,
- default='python')
- style = serializers.ChoiceField(choices=STYLE_CHOICES,
- default='friendly')
-
- def restore_object(self, attrs, instance=None):
+ language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python')
+ style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly')
+
+ def create(self, validated_data):
+ """
+ Create and return a new `Snippet` instance, given the validated data.
+ """
+ return Snippet.objects.create(**validated_data)
+
+ def update(self, instance, validated_data):
"""
- Create or update a new snippet instance, given a dictionary
- of deserialized field values.
-
- Note that if we don't define this method, then deserializing
- data will simply return a dictionary of items.
+ Update and return an existing `Snippet` instance, given the validated data.
"""
- if instance:
- # Update existing instance
- instance.title = attrs.get('title', instance.title)
- instance.code = attrs.get('code', instance.code)
- instance.linenos = attrs.get('linenos', instance.linenos)
- instance.language = attrs.get('language', instance.language)
- instance.style = attrs.get('style', instance.style)
- return instance
+ instance.title = validated_data.get('title', instance.title)
+ instance.code = validated_data.get('code', instance.code)
+ instance.linenos = validated_data.get('linenos', instance.linenos)
+ instance.language = validated_data.get('language', instance.language)
+ instance.style = validated_data.get('style', instance.style)
+ instance.save()
+ return instance
- # Create new instance
- return Snippet(**attrs)
+The first part of the serializer class defines the fields that get serialized/deserialized. The `create()` and `update()` methods define how fully fledged instances are created or modified when calling `serializer.save()`
-The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
+A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
-Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
+The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `{'base_template': 'textarea.html'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
-We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
+We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
## Working with Serializers
@@ -181,11 +159,11 @@ At this point we've translated the model instance into Python native datatypes.
content
# '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
-Deserialization is similar. First we parse a stream into Python native datatypes...
+Deserialization is similar. First we parse a stream into Python native datatypes...
- import StringIO
+ from django.utils.six import BytesIO
- stream = StringIO.StringIO(content)
+ stream = BytesIO(content)
data = JSONParser().parse(stream)
...then we restore those native datatypes into to a fully populated object instance.
@@ -193,9 +171,11 @@ Deserialization is similar. First we parse a stream into Python native datatype
serializer = SnippetSerializer(data=data)
serializer.is_valid()
# True
- serializer.object
+ serializer.validated_data
+ # OrderedDict([('title', ''), ('code', 'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])
+ serializer.save()
# <Snippet: Snippet object>
-
+
Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.
We can also serialize querysets instead of model instances. To do so we simply add a `many=True` flag to the serializer arguments.
@@ -211,13 +191,31 @@ Our `SnippetSerializer` class is replicating a lot of information that's also co
In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes.
Let's look at refactoring our serializer using the `ModelSerializer` class.
-Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class.
+Open the file `snippets/serializers.py` again, and replace the `SnippetSerializer` class with the following.
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
+One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing its representation. Open the Django shell with `python manage.py shell`, then try the following:
+
+ >>> from snippets.serializers import SnippetSerializer
+ >>> serializer = SnippetSerializer()
+ >>> print(repr(serializer))
+ SnippetSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ title = CharField(allow_blank=True, max_length=100, required=False)
+ code = CharField(style={'base_template': 'textarea.html'})
+ linenos = BooleanField(required=False)
+ language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
+ style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
+
+It's important to remember that `ModelSerializer` classes don't do anything particularly magical, they are simply a shortcut for creating serializer classes:
+
+* An automatically determined set of fields.
+* Simple default implementations for the `create()` and `update()` methods.
+
## Writing regular Django views using our Serializer
Let's see how we can write some API views using our new Serializer class.
@@ -225,7 +223,7 @@ For the moment we won't use any of REST framework's other features, we'll just w
We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`.
-Edit the `snippet/views.py` file, and add the following.
+Edit the `snippets/views.py` file, and add the following.
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
@@ -236,7 +234,7 @@ Edit the `snippet/views.py` file, and add the following.
class JSONResponse(HttpResponse):
"""
- An HttpResponse that renders it's content into JSON.
+ An HttpResponse that renders its content into JSON.
"""
def __init__(self, data, **kwargs):
content = JSONRenderer().render(data)
@@ -261,10 +259,9 @@ The root of our API is going to be a view that supports listing all the existing
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data, status=201)
- else:
- return JSONResponse(serializer.errors, status=400)
+ return JSONResponse(serializer.errors, status=400)
-Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
+Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet.
@@ -277,19 +274,18 @@ We'll also need a view which corresponds to an individual snippet, and can be us
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
-
+
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data)
-
+
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
- else:
- return JSONResponse(serializer.errors, status=400)
+ return JSONResponse(serializer.errors, status=400)
elif request.method == 'DELETE':
snippet.delete()
@@ -297,12 +293,13 @@ We'll also need a view which corresponds to an individual snippet, and can be us
Finally we need to wire these views up. Create the `snippets/urls.py` file:
- from django.conf.urls import patterns, url
+ from django.conf.urls import url
+ from snippets import views
- urlpatterns = patterns('snippets.views',
- url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail'),
- )
+ urlpatterns = [
+ url(r'^snippets/$', views.snippet_list),
+ url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
+ ]
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
@@ -327,17 +324,51 @@ Quit out of the shell...
In another terminal window, we can test the server.
-We can get a list of all of the snippets.
-
- curl http://127.0.0.1:8000/snippets/
-
- [{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}]
-
-Or we can get a particular snippet by referencing its id.
-
- curl http://127.0.0.1:8000/snippets/2/
-
- {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}
+We can test our API using using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that.
+
+You can install httpie using pip:
+
+ pip install httpie
+
+Finally, we can get a list of all of the snippets:
+
+ http http://127.0.0.1:8000/snippets/
+
+ HTTP/1.1 200 OK
+ ...
+ [
+ {
+ "id": 1,
+ "title": "",
+ "code": "foo = \"bar\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
+ ]
+
+Or we can get a particular snippet by referencing its id:
+
+ http http://127.0.0.1:8000/snippets/2/
+
+ HTTP/1.1 200 OK
+ ...
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
Similarly, you can have the same json displayed by visiting these URLs in a web browser.
@@ -354,3 +385,5 @@ We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
[sandbox]: http://restframework.herokuapp.com/
[virtualenv]: http://www.virtualenv.org/en/latest/index.html
[tut-2]: 2-requests-and-responses.md
+[httpie]: https://github.com/jakubroztocil/httpie#installation
+[curl]: http://curl.haxx.se
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index 30966a10..e2c173d6 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -5,10 +5,10 @@ Let's introduce a couple of essential building blocks.
## Request objects
-REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
+REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.data` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
request.POST # Only handles form data. Only works for 'POST' method.
- request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
+ request.data # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
## Response objects
@@ -29,13 +29,13 @@ REST framework provides two wrappers you can use to write API views.
These wrappers provide a few bits of functionality such as making sure you receive `Request` instances in your view, and adding context to `Response` objects so that content negotiation can be performed.
-The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input.
+The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.data` with malformed input.
## Pulling it all together
-Okay, let's go ahead and start using these new components to write a few views.
+Okay, let's go ahead and start using these new components to write a few views.
-We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
+We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
from rest_framework import status
from rest_framework.decorators import api_view
@@ -55,22 +55,21 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
return Response(serializer.data)
elif request.method == 'POST':
- serializer = SnippetSerializer(data=request.DATA)
+ serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
-Here is the view for an individual snippet.
+Here is the view for an individual snippet, in the `views.py` module.
@api_view(['GET', 'PUT', 'DELETE'])
def snippet_detail(request, pk):
"""
Retrieve, update or delete a snippet instance.
- """
+ """
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
@@ -81,12 +80,11 @@ Here is the view for an individual snippet.
return Response(serializer.data)
elif request.method == 'PUT':
- serializer = SnippetSerializer(snippet, data=request.DATA)
+ serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
- else:
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE':
snippet.delete()
@@ -94,11 +92,11 @@ Here is the view for an individual snippet.
This should all feel very familiar - it is not a lot different from working with regular Django views.
-Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
+Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.data` can handle incoming `json` requests, but it can also handle other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
## Adding optional format suffixes to our URLs
-To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4.json][json-url].
+To take advantage of the fact that our responses are no longer hardwired to a single content type let's add support for format suffixes to our API endpoints. Using format suffixes gives us URLs that explicitly refer to a given format, and means our API will be able to handle URLs such as [http://example.com/api/items/4/.json][json-url].
Start by adding a `format` keyword argument to both of the views, like so.
@@ -112,12 +110,13 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
+ from snippets import views
+
+ urlpatterns = [
+ url(r'^snippets/$', views.snippet_list),
+ url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
+ ]
- urlpatterns = patterns('snippets.views',
- url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'),
- )
-
urlpatterns = format_suffix_patterns(urlpatterns)
We don't necessarily need to add these extra url patterns in, but it gives us a simple, clean way of referring to a specific format.
@@ -128,31 +127,64 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
We can get a list of all of the snippets, as before.
- curl http://127.0.0.1:8000/snippets/
-
- [{"id": 1, "title": "", "code": "foo = \"bar\"\n", "linenos": false, "language": "python", "style": "friendly"}, {"id": 2, "title": "", "code": "print \"hello, world\"\n", "linenos": false, "language": "python", "style": "friendly"}]
+ http http://127.0.0.1:8000/snippets/
+
+ HTTP/1.1 200 OK
+ ...
+ [
+ {
+ "id": 1,
+ "title": "",
+ "code": "foo = \"bar\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ },
+ {
+ "id": 2,
+ "title": "",
+ "code": "print \"hello, world\"\n",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
+ ]
We can control the format of the response that we get back, either by using the `Accept` header:
- curl http://127.0.0.1:8000/snippets/ -H 'Accept: application/json' # Request JSON
- curl http://127.0.0.1:8000/snippets/ -H 'Accept: text/html' # Request HTML
+ http http://127.0.0.1:8000/snippets/ Accept:application/json # Request JSON
+ http http://127.0.0.1:8000/snippets/ Accept:text/html # Request HTML
Or by appending a format suffix:
- curl http://127.0.0.1:8000/snippets/.json # JSON suffix
- curl http://127.0.0.1:8000/snippets/.api # Browsable API suffix
+ http http://127.0.0.1:8000/snippets/.json # JSON suffix
+ http http://127.0.0.1:8000/snippets/.api # Browsable API suffix
Similarly, we can control the format of the request that we send, using the `Content-Type` header.
# POST using form data
- curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
+ http --form POST http://127.0.0.1:8000/snippets/ code="print 123"
- {"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"}
-
- # POST using JSON
- curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"
+ {
+ "id": 3,
+ "title": "",
+ "code": "print 123",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
- {"id": 4, "title": "", "code": "print 456", "linenos": true, "language": "python", "style": "friendly"}
+ # POST using JSON
+ http --json POST http://127.0.0.1:8000/snippets/ code="print 456"
+
+ {
+ "id": 4,
+ "title": "",
+ "code": "print 456",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index c1b3d8f2..abf82e49 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -4,7 +4,7 @@ We can also write our API views using class based views, rather than function ba
## Rewriting our API using class based views
-We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
+We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring of `views.py`.
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@@ -24,13 +24,13 @@ We'll start by rewriting the root view as a class based view. All this involves
return Response(serializer.data)
def post(self, request, format=None):
- serializer = SnippetSerializer(data=request.DATA)
+ serializer = SnippetSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view.
+So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`.
class SnippetDetail(APIView):
"""
@@ -49,7 +49,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
def put(self, request, pk, format=None):
snippet = self.get_object(pk)
- serializer = SnippetSerializer(snippet, data=request.DATA)
+ serializer = SnippetSerializer(snippet, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
@@ -62,17 +62,17 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
That's looking good. Again, it's still pretty similar to the function based view right now.
-We'll also need to refactor our URLconf slightly now we're using class based views.
+We'll also need to refactor our `urls.py` slightly now we're using class based views.
- from django.conf.urls import patterns, url
+ from django.conf.urls import url
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
- )
-
+ ]
+
urlpatterns = format_suffix_patterns(urlpatterns)
Okay, we're done. If you run the development server everything should be working just as before.
@@ -81,9 +81,9 @@ Okay, we're done. If you run the development server everything should be workin
One of the big wins of using class based views is that it allows us to easily compose reusable bits of behaviour.
-The create/retrieve/update/delete operations that we've been using so far are going to be pretty simliar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
+The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes.
-Let's take a look at how we can compose our views by using the mixin classes.
+Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again.
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor
## Using generic class based views
-Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
+Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down our `views.py` module even more.
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 393d879a..887d1e56 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -12,7 +12,7 @@ Currently our API doesn't have any restrictions on who can edit or delete code s
We're going to make a couple of changes to our `Snippet` model class.
First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code.
-Add the following two fields to the model.
+Add the following two fields to the `Snippet` model in `models.py`.
owner = models.ForeignKey('auth.User', related_name='snippets')
highlighted = models.TextField()
@@ -43,21 +43,23 @@ And now we can add a `.save()` method to our model class:
When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
- rm tmp.db
- python ./manage.py syncdb
+ rm -f tmp.db db.sqlite3
+ rm -r snippets/migrations
+ python manage.py makemigrations snippets
+ python manage.py migrate
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
- python ./manage.py createsuperuser
+ python manage.py createsuperuser
## Adding endpoints for our User models
-Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy:
+Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add:
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
- snippets = serializers.PrimaryKeyRelatedField(many=True)
+ snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
@@ -65,18 +67,25 @@ Now that we've got some users to work with, we'd better add representations of t
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
-We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
+We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
+
+ from django.contrib.auth.models import User
+
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
-
-
+
+
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
-Finally we need to add those views into the API, by referencing them from the URL conf.
+Make sure to also import the `UserSerializer` class
+
+ from snippets.serializers import UserSerializer
+
+Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`.
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
@@ -85,24 +94,26 @@ Finally we need to add those views into the API, by referencing them from the UR
Right now, if we created a code snippet, there'd be no way of associating the user that created the snippet, with the snippet instance. The user isn't sent as part of the serialized representation, but is instead a property of the incoming request.
-The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL.
+The way we deal with that is by overriding a `.perform_create()` method on our snippet views, that allows us to modify how the instance save is managed, and handle any information that is implicit in the incoming request or requested URL.
+
+On the `SnippetList` view class, add the following method:
-On **both** the `SnippetList` and `SnippetDetail` view classes, add the following method:
+ def perform_create(self, serializer):
+ serializer.save(owner=self.request.user)
- def pre_save(self, obj):
- obj.owner = self.request.user
+The `create()` method of our serializer will now be passed an additional `'owner'` field, along with the validated data from the request.
## Updating our serializer
-Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
+Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`:
- owner = serializers.Field(source='owner.username')
+ owner = serializers.ReadOnlyField(source='owner.username')
**Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class.
This field is doing something quite interesting. The `source` argument controls which attribute is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as it is used with Django's template language.
-The field we've added is the untyped `Field` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `Field` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized.
+The field we've added is the untyped `ReadOnlyField` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `ReadOnlyField` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized. We could have also used `CharField(read_only=True)` here.
## Adding required permissions to views
@@ -122,7 +133,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
-We can add a login view for use with the browsable API, by editing our URLconf once more.
+We can add a login view for use with the browsable API, by editing the URLconf in our project-level `urls.py` file.
Add the following import at the top of the file:
@@ -130,10 +141,10 @@ Add the following import at the top of the file:
And, at the end of the file, add a pattern to include the login and logout views for the browsable API.
- urlpatterns += patterns('',
+ urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
- )
+ ]
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
@@ -150,8 +161,8 @@ To do that we're going to need to create a custom permission.
In the snippets app, create a new file, `permissions.py`
from rest_framework import permissions
-
-
+
+
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
@@ -160,13 +171,13 @@ In the snippets app, create a new file, `permissions.py`
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
- if request.method in permissions.SAFE_METHODS:
+ if request.method in permissions.SAFE_METHODS:
return True
-
- # Write permissions are only allowed to the owner of the snippet
+
+ # Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
-Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
+Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` view class:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
@@ -187,15 +198,25 @@ If we're interacting with the API programmatically we need to explicitly provide
If we try to create a snippet without authenticating, we'll get an error:
- curl -i -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
+ http POST http://127.0.0.1:8000/snippets/ code="print 123"
- {"detail": "Authentication credentials were not provided."}
+ {
+ "detail": "Authentication credentials were not provided."
+ }
We can make a successful request by including the username and password of one of the users we created earlier.
- curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 789" -u tom:password
-
- {"id": 5, "owner": "tom", "title": "foo", "code": "print 789", "linenos": false, "language": "python", "style": "friendly"}
+ http -a tom:password POST http://127.0.0.1:8000/snippets/ code="print 789"
+
+ {
+ "id": 5,
+ "owner": "tom",
+ "title": "foo",
+ "code": "print 789",
+ "linenos": false,
+ "language": "python",
+ "style": "friendly"
+ }
## Summary
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index 2cf44bf9..91cdd6f1 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -1,12 +1,11 @@
# Tutorial 5: Relationships & Hyperlinked APIs
-At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
+At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
## Creating an endpoint for the root of our API
-Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier.
+Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add:
- from rest_framework import renderers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
@@ -29,7 +28,7 @@ Unlike all our other API endpoints, we don't want to use JSON, but instead just
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
-Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
+Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your `snippets/views.py` add:
from rest_framework import renderers
from rest_framework.response import Response
@@ -37,15 +36,15 @@ Instead of using a concrete generic view, we'll use the base class for represent
class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = (renderers.StaticHTMLRenderer,)
-
+
def get(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
As usual we need to add the new views that we've created in to our URLconf.
-We'll add a url pattern for our new API root:
+We'll add a url pattern for our new API root in `snippets/urls.py`:
- url(r'^$', 'api_root'),
+ url(r'^$', views.api_root),
And then add a url pattern for the snippet highlights:
@@ -73,21 +72,21 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial
* Relationships use `HyperlinkedRelatedField`,
instead of `PrimaryKeyRelatedField`.
-We can easily re-write our existing serializers to use hyperlinking.
+We can easily re-write our existing serializers to use hyperlinking. In your `snippets/serializers.py` add:
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
- owner = serializers.Field(source='owner.username')
+ owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
-
+
class Meta:
model = Snippet
fields = ('url', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
-
-
+
+
class UserSerializer(serializers.HyperlinkedModelSerializer):
- snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail')
-
+ snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail', read_only=True)
+
class Meta:
model = User
fields = ('url', 'username', 'snippets')
@@ -105,11 +104,13 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
* Our user serializer includes a field that refers to `'snippet-detail'`.
* Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`.
-After adding all those names into our URLconf, our final `'urls.py'` file should look something like this:
+After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this:
+
+ from django.conf.urls import url, include
# API endpoints
- urlpatterns = format_suffix_patterns(patterns('snippets.views',
- url(r'^$', 'api_root'),
+ urlpatterns = format_suffix_patterns([
+ url(r'^$', views.api_root),
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
@@ -125,22 +126,22 @@ After adding all those names into our URLconf, our final `'urls.py'` file should
url(r'^users/(?P<pk>[0-9]+)/$',
views.UserDetail.as_view(),
name='user-detail')
- ))
-
+ ])
+
# Login and logout views for the browsable API
- urlpatterns += patterns('',
+ urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
- )
+ ]
## Adding pagination
The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages.
-We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting:
+We can change the default list style to use pagination, by modifying our `tutorial/settings.py` file slightly. Add the following setting:
REST_FRAMEWORK = {
- 'PAGINATE_BY': 10
+ 'PAGE_SIZE': 10
}
Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well separated from your other project settings.
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index f16add39..63dff73f 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -10,7 +10,7 @@ A `ViewSet` class is only bound to a set of method handlers at the last moment,
Let's take our current set of views, and refactor them into view sets.
-First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace then with a single class:
+First of all let's refactor our `UserList` and `UserDetail` views into a single `UserViewSet`. We can remove the two views, and replace them with a single class:
from rest_framework import viewsets
@@ -21,11 +21,11 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single
queryset = User.objects.all()
serializer_class = UserSerializer
-Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
+Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
- from rest_framework.decorators import link
+ from rest_framework.decorators import detail_route
class SnippetViewSet(viewsets.ModelViewSet):
"""
@@ -39,19 +39,21 @@ Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighl
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
- @link(renderer_classes=[renderers.StaticHTMLRenderer])
+ @detail_route(renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
- def pre_save(self, obj):
- obj.owner = self.request.user
+ def perform_create(self, serializer):
+ serializer.save(owner=self.request.user)
This time we've used the `ModelViewSet` class in order to get the complete set of default read and write operations.
-Notice that we've also used the `@link` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style.
+Notice that we've also used the `@detail_route` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style.
-Custom actions which use the `@link` decorator will respond to `GET` requests. We could have instead used the `@action` decorator if we wanted an action that responded to `POST` requests.
+Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
+
+The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument.
## Binding ViewSets to URLs explicitly
@@ -60,7 +62,8 @@ To see what's going on under the hood let's first explicitly create a set of vie
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
- from snippets.views import SnippetViewSet, UserViewSet
+ from snippets.views import SnippetViewSet, UserViewSet, api_root
+ from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({
'get': 'list',
@@ -84,16 +87,16 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
-Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual.
+Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
- urlpatterns = format_suffix_patterns(patterns('snippets.views',
- url(r'^$', 'api_root'),
+ urlpatterns = format_suffix_patterns([
+ url(r'^$', api_root),
url(r'^snippets/$', snippet_list, name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
- ))
+ ])
## Using Routers
@@ -101,6 +104,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do
Here's our re-wired `urls.py` file.
+ from django.conf.urls import url, include
from snippets import views
from rest_framework.routers import DefaultRouter
@@ -110,11 +114,11 @@ Here's our re-wired `urls.py` file.
router.register(r'users', views.UserViewSet)
# The API URLs are now determined automatically by the router.
- # Additionally, we include the login URLs for the browseable API.
- urlpatterns = patterns('',
+ # Additionally, we include the login URLs for the browsable API.
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Registering the viewsets with the router is similar to providing a urlpattern. We include two arguments - the URL prefix for the views, and the viewset itself.
@@ -128,7 +132,7 @@ That doesn't mean it's always the right approach to take. There's a similar set
## Reviewing our work
-With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
+With an incredibly small amount of code, we've now got a complete pastebin Web API, which is fully web browsable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
@@ -136,7 +140,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
## Onwards and upwards
-We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
+We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index f15e75c0..fe0ecbc7 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -6,54 +6,48 @@ We're going to create a simple API to allow admin users to view and edit the use
Create a new Django project named `tutorial`, then start a new app called `quickstart`.
- # Set up a new project
- django-admin.py startproject tutorial
+ # Create the project directory
+ mkdir tutorial
cd tutorial
# Create a virtualenv to isolate our package dependencies locally
virtualenv env
- source env/bin/activate
+ source env/bin/activate # On Windows use `env\Scripts\activate`
# Install Django and Django REST framework into the virtualenv
pip install django
pip install djangorestframework
- # Create a new app
- python manage.py startapp quickstart
+ # Set up a new project with a single application
+ django-admin.py startproject tutorial . # Note the trailing '.' character
+ cd tutorial
+ django-admin.py startapp quickstart
+ cd ..
-Next you'll need to get a database set up and synced. If you just want to use SQLite for now, then you'll want to edit your `tutorial/settings.py` module to include something like this:
+Now sync your database for the first time:
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'database.sql',
- 'USER': '',
- 'PASSWORD': '',
- 'HOST': '',
- 'PORT': ''
- }
- }
+ python manage.py migrate
-The run `syncdb` like so:
+We'll also create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
- python manage.py syncdb
+ python manage.py createsuperuser
-Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
+Once you've set up a database and initial user created and ready to go, open up the app's directory and we'll get coding...
## Serializers
-First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
+First up we're going to define some serializers. Let's create a new module named `tutorial/quickstart/serializers.py` that we'll use for our data representations.
from django.contrib.auth.models import User, Group
from rest_framework import serializers
-
-
+
+
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
-
-
+
+
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
@@ -63,21 +57,21 @@ Notice that we're using hyperlinked relations in this case, with `HyperlinkedMod
## Views
-Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
+Right, we'd better write some views then. Open `tutorial/quickstart/views.py` and get typing.
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
- from quickstart.serializers import UserSerializer, GroupSerializer
-
-
+ from tutorial.quickstart.serializers import UserSerializer, GroupSerializer
+
+
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
-
-
+
+
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
@@ -85,28 +79,32 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty
queryset = Group.objects.all()
serializer_class = GroupSerializer
-Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`.
+Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`.
We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise.
+Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute.
+
+For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications.
+
## URLs
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
- from django.conf.urls import patterns, url, include
+ from django.conf.urls import url, include
from rest_framework import routers
- from quickstart import views
+ from tutorial.quickstart import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
# Wire up our API using automatic URL routing.
- # Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ # Additionally, we include login URLs for the browsable API.
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.
@@ -125,7 +123,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',),
- 'PAGINATE_BY': 10
+ 'PAGE_SIZE': 10
}
Okay, we're done.
@@ -140,35 +138,66 @@ We're now ready to test the API we've built. Let's fire up the server from the
We can now access our API, both from the command-line, using tools like `curl`...
- bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
{
- "count": 2,
- "next": null,
- "previous": null,
+ "count": 2,
+ "next": null,
+ "previous": null,
"results": [
{
- "email": "admin@example.com",
- "groups": [],
- "url": "http://127.0.0.1:8000/users/1/",
+ "email": "admin@example.com",
+ "groups": [],
+ "url": "http://127.0.0.1:8000/users/1/",
"username": "admin"
- },
+ },
{
- "email": "tom@example.com",
- "groups": [ ],
- "url": "http://127.0.0.1:8000/users/2/",
+ "email": "tom@example.com",
+ "groups": [ ],
+ "url": "http://127.0.0.1:8000/users/2/",
"username": "tom"
}
]
}
+Or using the [httpie][httpie], command line tool...
+
+ bash: http -a username:password http://127.0.0.1:8000/users/
+
+ HTTP/1.1 200 OK
+ ...
+ {
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "email": "admin@example.com",
+ "groups": [],
+ "url": "http://localhost:8000/users/1/",
+ "username": "paul"
+ },
+ {
+ "email": "tom@example.com",
+ "groups": [ ],
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "tom"
+ }
+ ]
+ }
+
+
Or directly through the browser...
![Quick start image][image]
+If you're working through the browser, make sure to login using the control in the top right corner.
+
Great, that was easy!
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
+[readme-example-api]: ../#example
[image]: ../img/quickstart.png
[tutorial]: 1-serialization.md
[guide]: ../#api-guide
+[httpie]: https://github.com/jakubroztocil/httpie#installation
diff --git a/docs_theme/404.html b/docs_theme/404.html
new file mode 100644
index 00000000..44993e37
--- /dev/null
+++ b/docs_theme/404.html
@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>Django REST framework - 404 - Page not found</title>
+ <link href="http://www.django-rest-framework.org/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="canonical" href="http://www.django-rest-framework.org/404" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="Django, API, REST, 404 - Page not found">
+ <meta name="author" content="Tom Christie">
+
+ <!-- Le styles -->
+ <link href="http://www.django-rest-framework.org/css/prettify.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/bootstrap-responsive.css" rel="stylesheet">
+ <link href="http://www.django-rest-framework.org/css/default.css" rel="stylesheet">
+
+ <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+ <!--[if lt IE 9]>
+ <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+ <![endif]-->
+
+ <script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', 'UA-18852272-2']);
+ _gaq.push(['_trackPageview']);
+
+ (function() {
+ var ga = document.createElement('script');
+ ga.type = 'text/javascript';
+ ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+</head>
+
+<body onload="prettyPrint()" class="404-page">
+
+ <div class="wrapper">
+
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
+ <a class="repo-link btn btn-inverse btn-small disabled" href="#">Next <i class="icon-arrow-right icon-white"></i></a>
+ <a class="repo-link btn btn-inverse btn-small disabled" href="#"><i class="icon-arrow-left icon-white"></i> Previous</a>
+ <a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
+ <div class="nav-collapse collapse">
+ <ul class="nav">
+ <li><a href="http://www.django-rest-framework.org">Home</a></li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/tutorial/quickstart">Quickstart</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/1-serialization">1 - Serialization</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/2-requests-and-responses">2 - Requests and responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/3-class-based-views">3 - Class based views</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions">4 - Authentication and permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/5-relationships-and-hyperlinked-apis">5 - Relationships and hyperlinked APIs</a></li>
+ <li><a href="http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers">6 - Viewsets and routers</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">API Guide <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/api-guide/requests">Requests</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/responses">Responses</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/views">Views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/generic-views">Generic views</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/viewsets">Viewsets</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/routers">Routers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/parsers">Parsers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/renderers">Renderers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/serializers">Serializers</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/fields">Serializer fields</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/relations">Serializer relations</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/authentication">Authentication</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/permissions">Permissions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/throttling">Throttling</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/filtering">Filtering</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/pagination">Pagination</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/content-negotiation">Content negotiation</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/format-suffixes">Format suffixes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/reverse">Returning URLs</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/exceptions">Exceptions</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/status-codes">Status codes</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/testing">Testing</a></li>
+ <li><a href="http://www.django-rest-framework.org/api-guide/settings">Settings</a></li>
+ </ul>
+ </li>
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="http://www.django-rest-framework.org/topics/documenting-your-api">Documenting your API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/ajax-csrf-cors">AJAX, CSRF & CORS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browser-enhancements">Browser enhancements</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/browsable-api">The Browsable API</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-hypermedia-hateoas">REST, Hypermedia & HATEOAS</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/rest-framework-2-announcement">2.0 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.2-announcement">2.2 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/2.3-announcement">2.3 Announcement</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/release-notes">Release Notes</a></li>
+ <li><a href="http://www.django-rest-framework.org/topics/credits">Credits</a></li>
+ </ul>
+ </li>
+ </ul>
+ <ul class="nav pull-right">
+ <!-- TODO
+ <li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ <li><a href="#">Trunk</a></li>
+ <li><a href="#">2.0.0</a></li>
+ </ul>
+ </li>
+ -->
+ </ul>
+ </div>
+ <!--/.nav-collapse -->
+ </div>
+ </div>
+ </div>
+
+ <div class="body-content">
+ <div class="container-fluid">
+ <!-- Search Modal -->
+ <div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h3 id="myModalLabel">Documentation search</h3>
+ </div>
+ <div class="modal-body">
+ <!-- Custom google search -->
+ <script>
+ (function() {
+ var cx = '015016005043623903336:rxraeohqk6w';
+ var gcse = document.createElement('script');
+ gcse.type = 'text/javascript';
+ gcse.async = true;
+ gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
+ '//www.google.com/cse/cse.js?cx=' + cx;
+ var s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(gcse, s);
+ })();
+ </script>
+ <gcse:search></gcse:search>
+ </div>
+ <div class="modal-footer">
+ <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
+ </div>
+ </div>
+
+ <div class="row-fluid">
+ <div id="main-content" class="span12">
+ <h1 id="404-page-not-found" style="text-align: center">404</h1>
+ <p style="text-align: center"><strong>Page not found</strong>
+ </p>
+ <p style="text-align: center">Try the <a href="http://www.django-rest-framework.org/">homepage</a>, or <a href="#searchModal" data-toggle="modal">search the documentation</a>.</p>
+ </div>
+ <!--/span-->
+ </div>
+ <!--/row-->
+ </div>
+ <!--/.fluid-container-->
+ </div>
+ <!--/.body content-->
+
+ <div id="push"></div>
+ </div>
+ <!--/.wrapper -->
+
+ <footer class="span12">
+ <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</a>
+ </p>
+ </footer>
+
+ <!-- Le javascript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="http://www.django-rest-framework.org/js/jquery-1.8.1-min.js"></script>
+ <script src="http://www.django-rest-framework.org/js/prettify-1.0.js"></script>
+ <script src="http://www.django-rest-framework.org/js/bootstrap-2.1.1-min.js"></script>
+ <script>
+ //$('.side-nav').scrollspy()
+ var shiftWindow = function() {
+ scrollBy(0, -50)
+ };
+ if (location.hash) shiftWindow();
+ window.addEventListener("hashchange", shiftWindow);
+
+ $('.dropdown-menu').on('click touchstart', function(event) {
+ event.stopPropagation();
+ });
+
+ // Dynamically force sidenav to no higher than browser window
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+
+ $(function() {
+ $(window).resize(function() {
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+ });
+ });
+ </script>
+</body>
+
+</html>
diff --git a/docs_theme/base.html b/docs_theme/base.html
new file mode 100644
index 00000000..b3066eb3
--- /dev/null
+++ b/docs_theme/base.html
@@ -0,0 +1,197 @@
+<!DOCTYPE html>
+<html lang="en">
+
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta charset="utf-8">
+ <title>{{ page_title }}</title>
+ <link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
+ <link rel="canonical" href="{{ canonical_url }}" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <meta name="description" content="Django, API, REST, {{ current_page.title }}">
+ <meta name="author" content="Tom Christie">
+
+ <!-- Le styles -->
+ <link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
+ <link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
+ <link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
+ <link href="{{ base_url }}/css/default.css" rel="stylesheet">
+
+ <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
+ <!--[if lt IE 9]>
+ <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
+ <![endif]-->
+
+ <script type="text/javascript">
+ var _gaq = _gaq || [];
+ _gaq.push(['_setAccount', 'UA-18852272-2']);
+ _gaq.push(['_trackPageview']);
+
+ (function() {
+ var ga = document.createElement('script');
+ ga.type = 'text/javascript';
+ ga.async = true;
+ ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+ var s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(ga, s);
+ })();
+ </script>
+
+ <style>
+ span.fusion-wrap a {
+ display: block;
+ margin-top: 10px;
+ color: black;
+ }
+ a.fusion-poweredby {
+ display: block;
+ margin-top: 10px;
+ }
+ @media (max-width: 767px) {
+ div.promo {
+ display: none;
+ }
+ }
+ </style>
+</head>
+<body onload="prettyPrint()" class="{% if current_page.is_homepage %}index{% endif %}-page">
+
+ <div class="wrapper">
+
+ {% include "nav.html" %}
+
+ <div class="body-content">
+ <div class="container-fluid">
+
+ <!-- Search Modal -->
+ <div id="searchModal" class="modal hide fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+ <div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+ <h3 id="myModalLabel">Documentation search</h3>
+ </div>
+
+ <div class="modal-body">
+ <!-- Custom google search -->
+ <script>
+ (function() {
+ var cx = '015016005043623903336:rxraeohqk6w';
+ var gcse = document.createElement('script');
+ gcse.type = 'text/javascript';
+ gcse.async = true;
+ gcse.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') +
+ '//www.google.com/cse/cse.js?cx=' + cx;
+ var s = document.getElementsByTagName('script')[0];
+ s.parentNode.insertBefore(gcse, s);
+ })();
+ </script>
+ <gcse:search></gcse:search>
+ </div>
+
+ <div class="modal-footer">
+ <button class="btn" data-dismiss="modal" aria-hidden="true">Close</button>
+ </div>
+ </div>
+
+ <div class="row-fluid">
+
+ <div class="span3">
+ <!-- TODO
+ <p style="margin-top: -12px">
+ <a class="btn btn-mini btn-primary" style="width: 60px">&laquo; previous</a>
+ <a class="btn btn-mini btn-primary" style="float: right; margin-right: 8px; width: 60px;">next &raquo;</a>
+ </p>
+ -->
+ <div id="table-of-contents">
+ <ul class="nav nav-list side-nav well sidebar-nav-fixed">
+
+ {% if current_page.is_homepage %}
+ <li class="main">
+ <a href="#">Django REST framework</a>
+ </li>
+ {% endif %}
+
+ {% for toc_item in toc %}
+
+ <li class="{% if not current_page.is_homepage %}main{% endif %}">
+ <a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
+ </li>
+
+ {% for toc_item in toc_item.children %}
+ <li>
+ <a href="{{ toc_item.url }}">{{ toc_item.title }}</a>
+ </li>
+ {% endfor %}
+
+ {% endfor %}
+
+ {% if current_page.is_homepage %}
+ <div class="promo">
+ <hr/>
+ <script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>
+ </div>
+ {% endif %}
+
+ </ul>
+
+ </div>
+ </div>
+
+ <div id="main-content" class="span9">
+ {% if meta.source %}
+ {% for filename in meta.source %}
+ <a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/{{ filename }}">
+ <span class="label label-info">{{ filename }}</span>
+ </a>
+ {% endfor %}
+ {% endif %}
+
+ {{ content }}
+ </div>
+ <!--/span-->
+ </div>
+ <!--/row-->
+ </div>
+ <!--/.fluid-container-->
+ </div>
+ <!--/.body content-->
+ <div id="push"></div>
+ </div>
+ <!--/.wrapper -->
+
+ <footer class="span12">
+ <p>Documentation built with <a href="http://www.mkdocs.org/">MkDocs</a>.</a>
+ </p>
+ </footer>
+
+ <!-- Le javascript
+ ================================================== -->
+ <!-- Placed at the end of the document so the pages load faster -->
+ <script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
+ <script src="{{ base_url }}/js/prettify-1.0.js"></script>
+ <script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
+ <script src="{{ base_url }}/js/theme.js"></script>
+
+ <script>
+ //$('.side-nav').scrollspy()
+ var shiftWindow = function() {
+ scrollBy(0, -50)
+ };
+ if (location.hash) shiftWindow();
+ window.addEventListener("hashchange", shiftWindow);
+
+ $('.dropdown-menu').on('click touchstart', function(event) {
+ event.stopPropagation();
+ });
+
+ // Dynamically force sidenav to no higher than browser window
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+
+ $(function() {
+ $(window).resize(function() {
+ $('.side-nav').css('max-height', window.innerHeight - 130);
+ });
+ });
+ </script>
+</body>
+
+</html>
diff --git a/docs/css/bootstrap-responsive.css b/docs_theme/css/bootstrap-responsive.css
index a8caf451..a8caf451 100755
--- a/docs/css/bootstrap-responsive.css
+++ b/docs_theme/css/bootstrap-responsive.css
diff --git a/docs/css/bootstrap.css b/docs_theme/css/bootstrap.css
index 53df6859..53df6859 100755
--- a/docs/css/bootstrap.css
+++ b/docs_theme/css/bootstrap.css
diff --git a/docs/css/default.css b/docs_theme/css/default.css
index af6a9cc0..d998fbee 100644
--- a/docs/css/default.css
+++ b/docs_theme/css/default.css
@@ -171,6 +171,25 @@ body{
background-attachment: fixed;
}
+
+#main-content h1:first-of-type {
+ margin-top: 0
+}
+
+#main-content h1, #main-content h2 {
+ font-weight: 300;
+ margin-top: 20px
+}
+
+#main-content h3, #main-content h4, #main-content h5 {
+ font-weight: 500;
+ margin-top: 15px
+}
+
+#main-content img {
+ display: block;
+ margin: 40px auto;
+}
/* custom navigation styles */
.navbar .navbar-inner{
@@ -181,7 +200,7 @@ body{
}
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
- color: white;
+ color: white;
}
.nav-list > .active > a, .navbar .navbar-inner .nav li a:hover {
@@ -190,8 +209,20 @@ body{
}
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
- color: #A30000;
+ color: #A30000;
}
+
+.dropdown-menu .active > a,
+.dropdown-menu .active > a:hover {
+ background-image: none;
+}
+
+.navbar-inverse .nav .dropdown .active > a,
+.navbar-inverse .nav .dropdown .active > a:hover,
+.navbar-inverse .nav .dropdown .active > a:focus {
+ background-color: #eeeeee;
+}
+
.navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee;
color: #c20000;
@@ -227,6 +258,10 @@ body a:hover{
}
}
+h1 code, h2 code, h3 code, h4 code, h5 code {
+ color: #333;
+}
+
/* sticky footer and footer */
html, body {
height: 100%;
@@ -307,3 +342,76 @@ table {
.side-nav {
overflow-y: scroll;
}
+
+
+ul.sponsor.diamond li a {
+ float: left;
+ width: 600px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 70px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 600px auto;
+ background-repeat: no-repeat;
+ font-size: 200%;
+}
+
+@media (max-width: 1000px) {
+ ul.sponsor.diamond li a {
+ float: left;
+ width: 300px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 40px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 280px auto;
+ background-repeat: no-repeat;
+ font-size: 150%;
+ }
+}
+
+ul.sponsor.platinum li a {
+ float: left;
+ width: 300px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 40px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 280px auto;
+ background-repeat: no-repeat;
+ font-size: 150%;
+}
+
+ul.sponsor.gold li a {
+ float: left;
+ width: 130px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 30px;
+ padding: 150px 0 0 0;
+ background-position: 0 50%;
+ background-size: 130px auto;
+ background-repeat: no-repeat;
+ font-size: 120%;
+}
+
+ul.sponsor.silver li a {
+ float: left;
+ width: 130px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 30px;
+ padding: 150px 0 0 0;
+ background-position: 0 50%;
+ background-size: 130px auto;
+ background-repeat: no-repeat;
+ font-size: 120%;
+}
+
+ul.sponsor {
+ list-style: none;
+ display: block;
+}
diff --git a/docs/css/prettify.css b/docs_theme/css/prettify.css
index d437aff6..d437aff6 100644
--- a/docs/css/prettify.css
+++ b/docs_theme/css/prettify.css
diff --git a/docs/img/favicon.ico b/docs_theme/img/favicon.ico
index 917892ad..917892ad 100644
--- a/docs/img/favicon.ico
+++ b/docs_theme/img/favicon.ico
Binary files differ
diff --git a/docs/img/glyphicons-halflings-white.png b/docs_theme/img/glyphicons-halflings-white.png
index 3bf6484a..3bf6484a 100644
--- a/docs/img/glyphicons-halflings-white.png
+++ b/docs_theme/img/glyphicons-halflings-white.png
Binary files differ
diff --git a/docs/img/glyphicons-halflings.png b/docs_theme/img/glyphicons-halflings.png
index a9969993..a9969993 100644
--- a/docs/img/glyphicons-halflings.png
+++ b/docs_theme/img/glyphicons-halflings.png
Binary files differ
diff --git a/docs/img/grid.png b/docs_theme/img/grid.png
index 878c3ed5..878c3ed5 100644
--- a/docs/img/grid.png
+++ b/docs_theme/img/grid.png
Binary files differ
diff --git a/docs/js/bootstrap-2.1.1-min.js b/docs_theme/js/bootstrap-2.1.1-min.js
index e0b220f4..e0b220f4 100755
--- a/docs/js/bootstrap-2.1.1-min.js
+++ b/docs_theme/js/bootstrap-2.1.1-min.js
diff --git a/docs/js/jquery-1.8.1-min.js b/docs_theme/js/jquery-1.8.1-min.js
index 3b8d15d0..3b8d15d0 100644
--- a/docs/js/jquery-1.8.1-min.js
+++ b/docs_theme/js/jquery-1.8.1-min.js
diff --git a/docs/js/prettify-1.0.js b/docs_theme/js/prettify-1.0.js
index eef5ad7e..eef5ad7e 100644
--- a/docs/js/prettify-1.0.js
+++ b/docs_theme/js/prettify-1.0.js
diff --git a/docs_theme/js/theme.js b/docs_theme/js/theme.js
new file mode 100644
index 00000000..73607be5
--- /dev/null
+++ b/docs_theme/js/theme.js
@@ -0,0 +1,5 @@
+$(function(){
+
+ $('pre code').parent().addClass('prettyprint well');
+
+});
diff --git a/docs_theme/nav.html b/docs_theme/nav.html
new file mode 100644
index 00000000..ca1afc0e
--- /dev/null
+++ b/docs_theme/nav.html
@@ -0,0 +1,47 @@
+ <div class="navbar navbar-inverse navbar-fixed-top">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
+ <a class="repo-link btn btn-inverse btn-small {% if not next_page %}disabled{% endif %}" rel="prev" {% if next_page %}href="{{ next_page.url }}"{% endif %}>
+ Next <i class="icon-arrow-right icon-white"></i>
+ </a>
+ <a class="repo-link btn btn-inverse btn-small {% if not previous_page %}disabled{% endif %}" rel="next" {% if previous_page %}href="{{ previous_page.url }}"{% endif %}>
+ <i class="icon-arrow-left icon-white"></i> Previous
+ </a>
+ <a class="repo-link btn btn-inverse btn-small" href="#searchModal" data-toggle="modal"><i class="icon-search icon-white"></i> Search</a>
+ <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ <span class="icon-bar"></span>
+ </a>
+ <a class="brand" href="http://www.django-rest-framework.org">Django REST framework</a>
+ <div class="nav-collapse collapse">
+ {% if include_nav %}
+ <!-- Main navigation -->
+ <ul class="nav navbar-nav">
+ <li {% if current_page.is_homepage %}class="active"{% endif %}><a href="/">Home</a></li>
+ {% for nav_item in nav %} {% if nav_item.children %}
+ <li class="dropdown{% if nav_item.active %} active{% endif %}">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">{{ nav_item.title }} <b class="caret"></b></a>
+ <ul class="dropdown-menu">
+ {% for nav_item in nav_item.children %}
+ <li {% if nav_item.active %}class="active" {% endif %}>
+ <a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ </li>
+ {% else %}
+ <li {% if nav_item.active %}class="active" {% endif %}>
+ <a href="{{ nav_item.url }}">{{ nav_item.title }}</a>
+ </li>
+ {% endif %} {% endfor %}
+
+ </ul>
+ {% endif %}
+ </div>
+ <!--/.nav-collapse -->
+
+ </div>
+ </div>
+ </div>
diff --git a/mkdocs.py b/mkdocs.py
deleted file mode 100755
index 1e3f1db3..00000000
--- a/mkdocs.py
+++ /dev/null
@@ -1,185 +0,0 @@
-#!/usr/bin/env python
-
-import markdown
-import os
-import re
-import shutil
-import sys
-
-root_dir = os.path.abspath(os.path.dirname(__file__))
-docs_dir = os.path.join(root_dir, 'docs')
-html_dir = os.path.join(root_dir, 'html')
-
-local = not '--deploy' in sys.argv
-preview = '-p' in sys.argv
-
-if local:
- base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
- suffix = '.html'
- index = 'index.html'
-else:
- base_url = 'http://django-rest-framework.org'
- suffix = '.html'
- index = ''
-
-
-main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
-sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
-code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/\1"><span class="label label-info">\1</span></a>'
-
-page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
-
-# Copy static files
-# for static in ['css', 'js', 'img']:
-# source = os.path.join(docs_dir, 'static', static)
-# target = os.path.join(html_dir, static)
-# if os.path.exists(target):
-# shutil.rmtree(target)
-# shutil.copytree(source, target)
-
-
-# Hacky, but what the hell, it'll do the job
-path_list = [
- 'index.md',
- 'tutorial/quickstart.md',
- 'tutorial/1-serialization.md',
- 'tutorial/2-requests-and-responses.md',
- 'tutorial/3-class-based-views.md',
- 'tutorial/4-authentication-and-permissions.md',
- 'tutorial/5-relationships-and-hyperlinked-apis.md',
- 'tutorial/6-viewsets-and-routers.md',
- 'api-guide/requests.md',
- 'api-guide/responses.md',
- 'api-guide/views.md',
- 'api-guide/generic-views.md',
- 'api-guide/viewsets.md',
- 'api-guide/routers.md',
- 'api-guide/parsers.md',
- 'api-guide/renderers.md',
- 'api-guide/serializers.md',
- 'api-guide/fields.md',
- 'api-guide/relations.md',
- 'api-guide/authentication.md',
- 'api-guide/permissions.md',
- 'api-guide/throttling.md',
- 'api-guide/filtering.md',
- 'api-guide/pagination.md',
- 'api-guide/content-negotiation.md',
- 'api-guide/format-suffixes.md',
- 'api-guide/reverse.md',
- 'api-guide/exceptions.md',
- 'api-guide/status-codes.md',
- 'api-guide/settings.md',
- 'topics/documenting-your-api.md',
- 'topics/ajax-csrf-cors.md',
- 'topics/browser-enhancements.md',
- 'topics/browsable-api.md',
- 'topics/rest-hypermedia-hateoas.md',
- 'topics/contributing.md',
- 'topics/rest-framework-2-announcement.md',
- 'topics/2.2-announcement.md',
- 'topics/2.3-announcement.md',
- 'topics/release-notes.md',
- 'topics/credits.md',
-]
-
-prev_url_map = {}
-next_url_map = {}
-for idx in range(len(path_list)):
- path = path_list[idx]
- rel = '../' * path.count('/')
-
- if idx > 0:
- prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix
-
- if idx < len(path_list) - 1:
- next_url_map[path] = rel + path_list[idx + 1][:-3] + suffix
-
-
-for (dirpath, dirnames, filenames) in os.walk(docs_dir):
- relative_dir = dirpath.replace(docs_dir, '').lstrip(os.path.sep)
- build_dir = os.path.join(html_dir, relative_dir)
-
- if not os.path.exists(build_dir):
- os.makedirs(build_dir)
-
- for filename in filenames:
- path = os.path.join(dirpath, filename)
- relative_path = os.path.join(relative_dir, filename)
-
- if not filename.endswith('.md'):
- if relative_dir:
- output_path = os.path.join(build_dir, filename)
- shutil.copy(path, output_path)
- continue
-
- output_path = os.path.join(build_dir, filename[:-3] + '.html')
-
- toc = ''
- text = open(path, 'r').read().decode('utf-8')
- main_title = None
- description = 'Django, API, REST'
- for line in text.splitlines():
- if line.startswith('# '):
- title = line[2:].strip()
- template = main_header
- description = description + ', ' + title
- elif line.startswith('## '):
- title = line[3:].strip()
- template = sub_header
- else:
- continue
-
- if not main_title:
- main_title = title
- anchor = title.lower().replace(' ', '-').replace(':-', '-').replace("'", '').replace('?', '').replace('.', '')
- template = template.replace('{{ title }}', title)
- template = template.replace('{{ anchor }}', anchor)
- toc += template + '\n'
-
- if filename == 'index.md':
- main_title = 'Django REST framework - APIs made easy'
- else:
- main_title = 'Django REST framework - ' + main_title
-
- prev_url = prev_url_map.get(relative_path)
- next_url = next_url_map.get(relative_path)
-
- content = markdown.markdown(text, ['headerid'])
-
- output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
- output = output.replace('{{ title }}', main_title)
- output = output.replace('{{ description }}', description)
- output = output.replace('{{ page_id }}', filename[:-3])
-
- if prev_url:
- output = output.replace('{{ prev_url }}', prev_url)
- output = output.replace('{{ prev_url_disabled }}', '')
- else:
- output = output.replace('{{ prev_url }}', '#')
- output = output.replace('{{ prev_url_disabled }}', 'disabled')
-
- if next_url:
- output = output.replace('{{ next_url }}', next_url)
- output = output.replace('{{ next_url_disabled }}', '')
- else:
- output = output.replace('{{ next_url }}', '#')
- output = output.replace('{{ next_url_disabled }}', 'disabled')
-
- output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
- output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
- output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
- output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
- open(output_path, 'w').write(output.encode('utf-8'))
-
-if preview:
- import subprocess
-
- url = 'html/index.html'
-
- try:
- subprocess.Popen(["open", url]) # Mac
- except OSError:
- subprocess.Popen(["xdg-open", url]) # Linux
- except:
- os.startfile(url) # Windows
diff --git a/mkdocs.yml b/mkdocs.yml
new file mode 100644
index 00000000..8aacc2df
--- /dev/null
+++ b/mkdocs.yml
@@ -0,0 +1,56 @@
+site_name: Django REST framework
+site_url: http://www.django-rest-framework.org/
+site_description: Django REST framework - Web APIs for Django
+
+repo_url: https://github.com/tomchristie/django-rest-framework
+
+theme_dir: docs_theme
+
+pages:
+ - ['index.md', 'Home']
+ - ['tutorial/quickstart.md', 'Tutorial', 'Quickstart']
+ - ['tutorial/1-serialization.md', 'Tutorial', '1 - Serialization']
+ - ['tutorial/2-requests-and-responses.md', 'Tutorial', '2 - Requests and responses']
+ - ['tutorial/3-class-based-views.md', 'Tutorial', '3 - Class based views']
+ - ['tutorial/4-authentication-and-permissions.md', 'Tutorial', '4 - Authentication and permissions']
+ - ['tutorial/5-relationships-and-hyperlinked-apis.md', 'Tutorial', '5 - Relationships and hyperlinked APIs']
+ - ['tutorial/6-viewsets-and-routers.md', 'Tutorial', '6 - Viewsets and routers']
+ - ['api-guide/requests.md', 'API Guide', 'Requests']
+ - ['api-guide/responses.md', 'API Guide', 'Responses']
+ - ['api-guide/views.md', 'API Guide', 'Views']
+ - ['api-guide/generic-views.md', 'API Guide', 'Generic views']
+ - ['api-guide/viewsets.md', 'API Guide', 'Viewsets']
+ - ['api-guide/routers.md', 'API Guide', 'Routers']
+ - ['api-guide/parsers.md', 'API Guide', 'Parsers']
+ - ['api-guide/renderers.md', 'API Guide', 'Renderers']
+ - ['api-guide/serializers.md', 'API Guide', 'Serializers']
+ - ['api-guide/fields.md', 'API Guide', 'Serializer fields']
+ - ['api-guide/relations.md', 'API Guide', 'Serializer relations']
+ - ['api-guide/validators.md', 'API Guide', 'Validators']
+ - ['api-guide/authentication.md', 'API Guide', 'Authentication']
+ - ['api-guide/permissions.md', 'API Guide', 'Permissions']
+ - ['api-guide/throttling.md', 'API Guide', 'Throttling']
+ - ['api-guide/filtering.md', 'API Guide', 'Filtering']
+ - ['api-guide/pagination.md', 'API Guide', 'Pagination']
+ - ['api-guide/versioning.md', 'API Guide', 'Versioning']
+ - ['api-guide/content-negotiation.md', 'API Guide', 'Content negotiation']
+ - ['api-guide/metadata.md', 'API Guide', 'Metadata']
+ - ['api-guide/format-suffixes.md', 'API Guide', 'Format suffixes']
+ - ['api-guide/reverse.md', 'API Guide', 'Returning URLs']
+ - ['api-guide/exceptions.md', 'API Guide', 'Exceptions']
+ - ['api-guide/status-codes.md', 'API Guide', 'Status codes']
+ - ['api-guide/testing.md', 'API Guide', 'Testing']
+ - ['api-guide/settings.md', 'API Guide', 'Settings']
+ - ['topics/documenting-your-api.md', 'Topics', 'Documenting your API']
+ - ['topics/internationalization.md', 'Topics', 'Internationalization']
+ - ['topics/ajax-csrf-cors.md', 'Topics', 'AJAX, CSRF & CORS']
+ - ['topics/browser-enhancements.md', 'Topics',]
+ - ['topics/browsable-api.md', 'Topics', 'The Browsable API']
+ - ['topics/rest-hypermedia-hateoas.md', 'Topics', 'REST, Hypermedia & HATEOAS']
+ - ['topics/third-party-resources.md', 'Topics', 'Third Party Resources']
+ - ['topics/contributing.md', 'Topics', 'Contributing to REST framework']
+ - ['topics/project-management.md', 'Topics', 'Project management']
+ - ['topics/3.0-announcement.md', 'Topics', '3.0 Announcement']
+ - ['topics/3.1-announcement.md', 'Topics', '3.1 Announcement']
+ - ['topics/kickstarter-announcement.md', 'Topics', 'Kickstarter Announcement']
+ - ['topics/release-notes.md', 'Topics', 'Release Notes']
diff --git a/optionals.txt b/optionals.txt
deleted file mode 100644
index 4ebfceab..00000000
--- a/optionals.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-markdown>=2.1.0
-PyYAML>=3.10
-defusedxml>=0.3
-django-filter>=0.5.4
-django-oauth-plus>=2.0
-oauth2>=1.5.211
-django-oauth2-provider>=0.2.4
diff --git a/requirements.txt b/requirements.txt
index 730c1d07..4ec84f68 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,13 @@
-Django>=1.3
+# The base set of requirements for REST framework is actually
+# just Django, but for the purposes of development and testing
+# there are a number of packages that it is useful to install.
+
+# Laying these out as seperate requirements files, allows us to
+# only included the relevent sets when running tox, and ensures
+# we are only ever declaring out dependancies in one place.
+
+-r requirements/requirements-optionals.txt
+-r requirements/requirements-testing.txt
+-r requirements/requirements-documentation.txt
+-r requirements/requirements-codestyle.txt
+-r requirements/requirements-packaging.txt
diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt
new file mode 100644
index 00000000..4e2be24c
--- /dev/null
+++ b/requirements/requirements-codestyle.txt
@@ -0,0 +1,3 @@
+# PEP8 code linting, which we run on all commits.
+flake8==2.3.0
+pep8==1.6.2
diff --git a/requirements/requirements-documentation.txt b/requirements/requirements-documentation.txt
new file mode 100644
index 00000000..5009436e
--- /dev/null
+++ b/requirements/requirements-documentation.txt
@@ -0,0 +1,2 @@
+# MkDocs to build our documentation.
+mkdocs==0.11.1
diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt
new file mode 100644
index 00000000..af9937cf
--- /dev/null
+++ b/requirements/requirements-optionals.txt
@@ -0,0 +1,4 @@
+# Optional packages which may be used with REST framework.
+markdown==2.5.2
+django-guardian==1.2.5
+django-filter==0.9.2
diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt
new file mode 100644
index 00000000..1efb2f83
--- /dev/null
+++ b/requirements/requirements-packaging.txt
@@ -0,0 +1,8 @@
+# Wheel for PyPI installs.
+wheel==0.24.0
+
+# Twine for secured PyPI uploads.
+twine==1.4.0
+
+# Transifex client for managing translation resources.
+transifex-client==0.10
diff --git a/requirements/requirements-testing.txt b/requirements/requirements-testing.txt
new file mode 100644
index 00000000..a8d5d322
--- /dev/null
+++ b/requirements/requirements-testing.txt
@@ -0,0 +1,3 @@
+# PyTest for running the tests.
+pytest==2.6.4
+pytest-django==2.8.0
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 776618ac..f8bbeee3 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,6 +1,20 @@
-__version__ = '2.3.6'
+"""
+______ _____ _____ _____ __
+| ___ \ ___/ ___|_ _| / _| | |
+| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
+| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
+| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
+\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
+"""
-VERSION = __version__ # synonym
+__title__ = 'Django REST framework'
+__version__ = '3.1.0'
+__author__ = 'Tom Christie'
+__license__ = 'BSD 2-Clause'
+__copyright__ = 'Copyright 2011-2015 Tom Christie'
+
+# Version synonym
+VERSION = __version__
# Header encoding (see RFC5987)
HTTP_HEADER_ENCODING = 'iso-8859-1'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index cf001a24..f0702286 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -3,13 +3,10 @@ Provides various authentication policies.
"""
from __future__ import unicode_literals
import base64
-
from django.contrib.auth import authenticate
-from django.core.exceptions import ImproperlyConfigured
+from django.middleware.csrf import CsrfViewMiddleware
+from django.utils.translation import ugettext_lazy as _
from rest_framework import exceptions, HTTP_HEADER_ENCODING
-from rest_framework.compat import CsrfViewMiddleware
-from rest_framework.compat import oauth, oauth_provider, oauth_provider_store
-from rest_framework.compat import oauth2_provider, provider_now
from rest_framework.authtoken.models import Token
@@ -20,7 +17,7 @@ def get_authorization_header(request):
Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
- if type(auth) == type(''):
+ if isinstance(auth, type('')):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
@@ -69,16 +66,16 @@ class BasicAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid basic header. No credentials provided.'
+ msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid basic header. Credentials string should not contain spaces.'
+ msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError):
- msg = 'Invalid basic header. Credentials not correctly base64 encoded'
+ msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
@@ -89,8 +86,13 @@ class BasicAuthentication(BaseAuthentication):
Authenticate the userid and password against username and password.
"""
user = authenticate(username=userid, password=password)
- if user is None or not user.is_active:
- raise exceptions.AuthenticationFailed('Invalid username/password')
+
+ if user is None:
+ raise exceptions.AuthenticationFailed(_('Invalid username/password.'))
+
+ if not user.is_active:
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
+
return (user, None)
def authenticate_header(self, request):
@@ -128,7 +130,7 @@ class SessionAuthentication(BaseAuthentication):
reason = CSRFCheck().process_view(request, None, (), {})
if reason:
# CSRF failed, bail with explicit error message
- raise exceptions.AuthenticationFailed('CSRF Failed: %s' % reason)
+ raise exceptions.PermissionDenied('CSRF Failed: %s' % reason)
class TokenAuthentication(BaseAuthentication):
@@ -156,193 +158,24 @@ class TokenAuthentication(BaseAuthentication):
return None
if len(auth) == 1:
- msg = 'Invalid token header. No credentials provided.'
+ msg = _('Invalid token header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
- msg = 'Invalid token header. Token string should not contain spaces.'
+ msg = _('Invalid token header. Token string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
return self.authenticate_credentials(auth[1])
def authenticate_credentials(self, key):
try:
- token = self.model.objects.get(key=key)
+ token = self.model.objects.select_related('user').get(key=key)
except self.model.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
+ raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
- raise exceptions.AuthenticationFailed('User inactive or deleted')
+ raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
def authenticate_header(self, request):
return 'Token'
-
-
-class OAuthAuthentication(BaseAuthentication):
- """
- OAuth 1.0a authentication backend using `django-oauth-plus` and `oauth2`.
-
- Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- We import it from the `compat` module as `oauth`.
- """
- www_authenticate_realm = 'api'
-
- def __init__(self, *args, **kwargs):
- super(OAuthAuthentication, self).__init__(*args, **kwargs)
-
- if oauth is None:
- raise ImproperlyConfigured(
- "The 'oauth2' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- if oauth_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth-plus' package could not be imported."
- "It is required for use with the 'OAuthAuthentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
- try:
- oauth_request = oauth_provider.utils.get_oauth_request(request)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- if not oauth_request:
- return None
-
- oauth_params = oauth_provider.consts.OAUTH_PARAMETERS_NAMES
-
- found = any(param for param in oauth_params if param in oauth_request)
- missing = list(param for param in oauth_params if param not in oauth_request)
-
- if not found:
- # OAuth authentication was not attempted.
- return None
-
- if missing:
- # OAuth was attempted but missing parameters.
- msg = 'Missing parameters: %s' % (', '.join(missing))
- raise exceptions.AuthenticationFailed(msg)
-
- if not self.check_nonce(request, oauth_request):
- msg = 'Nonce check failed'
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- consumer_key = oauth_request.get_parameter('oauth_consumer_key')
- consumer = oauth_provider_store.get_consumer(request, oauth_request, consumer_key)
- except oauth_provider.store.InvalidConsumerError:
- msg = 'Invalid consumer token: %s' % oauth_request.get_parameter('oauth_consumer_key')
- raise exceptions.AuthenticationFailed(msg)
-
- if consumer.status != oauth_provider.consts.ACCEPTED:
- msg = 'Invalid consumer key status: %s' % consumer.get_status_display()
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- token_param = oauth_request.get_parameter('oauth_token')
- token = oauth_provider_store.get_access_token(request, oauth_request, consumer, token_param)
- except oauth_provider.store.InvalidTokenError:
- msg = 'Invalid access token: %s' % oauth_request.get_parameter('oauth_token')
- raise exceptions.AuthenticationFailed(msg)
-
- try:
- self.validate_token(request, consumer, token)
- except oauth.Error as err:
- raise exceptions.AuthenticationFailed(err.message)
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
- raise exceptions.AuthenticationFailed(msg)
-
- return (token.user, token)
-
- def authenticate_header(self, request):
- """
- If permission is denied, return a '401 Unauthorized' response,
- with an appropraite 'WWW-Authenticate' header.
- """
- return 'OAuth realm="%s"' % self.www_authenticate_realm
-
- def validate_token(self, request, consumer, token):
- """
- Check the token and raise an `oauth.Error` exception if invalid.
- """
- oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request)
- oauth_server.verify_request(oauth_request, consumer, token)
-
- def check_nonce(self, request, oauth_request):
- """
- Checks nonce of request, and return True if valid.
- """
- return oauth_provider_store.check_nonce(request, oauth_request, oauth_request['oauth_nonce'])
-
-
-class OAuth2Authentication(BaseAuthentication):
- """
- OAuth 2 authentication backend using `django-oauth2-provider`
- """
- www_authenticate_realm = 'api'
-
- def __init__(self, *args, **kwargs):
- super(OAuth2Authentication, self).__init__(*args, **kwargs)
-
- if oauth2_provider is None:
- raise ImproperlyConfigured(
- "The 'django-oauth2-provider' package could not be imported. "
- "It is required for use with the 'OAuth2Authentication' class.")
-
- def authenticate(self, request):
- """
- Returns two-tuple of (user, token) if authentication succeeds,
- or None otherwise.
- """
-
- auth = get_authorization_header(request).split()
-
- if not auth or auth[0].lower() != b'bearer':
- return None
-
- if len(auth) == 1:
- msg = 'Invalid bearer header. No credentials provided.'
- raise exceptions.AuthenticationFailed(msg)
- elif len(auth) > 2:
- msg = 'Invalid bearer header. Token string should not contain spaces.'
- raise exceptions.AuthenticationFailed(msg)
-
- return self.authenticate_credentials(request, auth[1])
-
- def authenticate_credentials(self, request, access_token):
- """
- Authenticate the request, given the access token.
- """
-
- try:
- token = oauth2_provider.models.AccessToken.objects.select_related('user')
- # provider_now switches to timezone aware datetime when
- # the oauth2_provider version supports to it.
- token = token.get(token=access_token, expires__gt=provider_now())
- except oauth2_provider.models.AccessToken.DoesNotExist:
- raise exceptions.AuthenticationFailed('Invalid token')
-
- user = token.user
-
- if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
- raise exceptions.AuthenticationFailed(msg)
-
- return (user, token)
-
- def authenticate_header(self, request):
- """
- Bearer is the only finalized type currently
-
- Check details on the `OAuth2Authentication.authenticate` method
- """
- return 'Bearer realm="%s"' % self.www_authenticate_realm
diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py
index d5965e40..769f6202 100644
--- a/rest_framework/authtoken/migrations/0001_initial.py
+++ b/rest_framework/authtoken/migrations/0001_initial.py
@@ -1,67 +1,26 @@
# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-from rest_framework.settings import api_settings
-
-
-try:
- from django.contrib.auth import get_user_model
-except ImportError: # django < 1.5
- from django.contrib.auth.models import User
-else:
- User = get_user_model()
-
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
- # Adding model 'Token'
- db.create_table('authtoken_token', (
- ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
- ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
- ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
- ))
- db.send_create_signal('authtoken', ['Token'])
-
-
- def backwards(self, orm):
- # Deleting model 'Token'
- db.delete_table('authtoken_token')
-
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- "%s.%s" % (User._meta.app_label, User._meta.module_name): {
- 'Meta': {'object_name': User._meta.module_name},
- },
- 'authtoken.token': {
- 'Meta': {'object_name': 'Token'},
- 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
- 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
- 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- }
- }
-
- complete_apps = ['authtoken']
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ ('key', models.CharField(primary_key=True, serialize=False, max_length=40)),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='auth_token')),
+ ],
+ options={
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 7601f5b7..a1a9315f 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,11 +1,19 @@
-import uuid
-import hmac
-from hashlib import sha1
-from rest_framework.compat import AUTH_USER_MODEL
+import binascii
+import os
+
from django.conf import settings
from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+# Prior to Django 1.5, the AUTH_USER_MODEL setting does not exist.
+# Note that we don't perform this code in the compat module due to
+# bug report #1297
+# See: https://github.com/tomchristie/django-rest-framework/issues/1297
+AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
+@python_2_unicode_compatible
class Token(models.Model):
"""
The default authorization token model.
@@ -28,8 +36,7 @@ class Token(models.Model):
return super(Token, self).save(*args, **kwargs)
def generate_key(self):
- unique = uuid.uuid4()
- return hmac.new(unique.bytes, digestmod=sha1).hexdigest()
+ return binascii.hexlify(os.urandom(20)).decode()
- def __unicode__(self):
+ def __str__(self):
return self.key
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 60a3740e..37ade255 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -1,5 +1,7 @@
from django.contrib.auth import authenticate
-from rest_framework import serializers
+from django.utils.translation import ugettext_lazy as _
+
+from rest_framework import exceptions, serializers
class AuthTokenSerializer(serializers.Serializer):
@@ -15,10 +17,14 @@ class AuthTokenSerializer(serializers.Serializer):
if user:
if not user.is_active:
- raise serializers.ValidationError('User account is disabled.')
- attrs['user'] = user
- return attrs
+ msg = _('User account is disabled.')
+ raise exceptions.ValidationError(msg)
else:
- raise serializers.ValidationError('Unable to login with provided credentials.')
+ msg = _('Unable to log in with provided credentials.')
+ raise exceptions.ValidationError(msg)
else:
- raise serializers.ValidationError('Must include "username" and "password"')
+ msg = _('Must include "username" and "password".')
+ raise exceptions.ValidationError(msg)
+
+ attrs['user'] = user
+ return attrs
diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py
new file mode 100644
index 00000000..5b927f3e
--- /dev/null
+++ b/rest_framework/authtoken/south_migrations/0001_initial.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from south.v2 import SchemaMigration
+
+try:
+ from django.contrib.auth import get_user_model
+except ImportError: # django < 1.5
+ from django.contrib.auth.models import User
+else:
+ User = get_user_model()
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Token'
+ db.create_table('authtoken_token', (
+ ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('authtoken', ['Token'])
+
+ def backwards(self, orm):
+ # Deleting model 'Token'
+ db.delete_table('authtoken_token')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ "%s.%s" % (User._meta.app_label, User._meta.module_name): {
+ 'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)},
+ },
+ 'authtoken.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['authtoken']
diff --git a/rest_framework/runtests/__init__.py b/rest_framework/authtoken/south_migrations/__init__.py
index e69de29b..e69de29b 100644
--- a/rest_framework/runtests/__init__.py
+++ b/rest_framework/authtoken/south_migrations/__init__.py
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 7c03cb76..b75c2e25 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -1,5 +1,4 @@
from rest_framework.views import APIView
-from rest_framework import status
from rest_framework import parsers
from rest_framework import renderers
from rest_framework.response import Response
@@ -12,15 +11,13 @@ class ObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
- serializer_class = AuthTokenSerializer
- model = Token
def post(self, request):
- serializer = self.serializer_class(data=request.DATA)
- if serializer.is_valid():
- token, created = Token.objects.get_or_create(user=serializer.object['user'])
- return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer = AuthTokenSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ user = serializer.validated_data['user']
+ token, created = Token.objects.get_or_create(user=user)
+ return Response({'token': token.key})
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 6f7447ad..c6a4a869 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -5,34 +5,58 @@ versions of django/python, and compatibility wrappers around optional packages.
# flake8: noqa
from __future__ import unicode_literals
-
-import django
from django.core.exceptions import ImproperlyConfigured
from django.conf import settings
-
-# Try to import six from Django, fallback to included `six`.
+from django.utils.encoding import force_text
+from django.utils.six.moves.urllib.parse import urlparse as _urlparse
+from django.utils import six
+import django
+import inspect
try:
- from django.utils import six
+ import importlib
except ImportError:
- from rest_framework import six
+ from django.utils import importlib
-# location of patterns, url, include changes in 1.4 onwards
-try:
- from django.conf.urls import patterns, url, include
-except ImportError:
- from django.conf.urls.defaults import patterns, url, include
+def unicode_repr(instance):
+ # Get the repr of an instance, but ensure it is a unicode string
+ # on both python 3 (already the case) and 2 (not the case).
+ if six.PY2:
+ return repr(instance).decode('utf-8')
+ return repr(instance)
-# Handle django.utils.encoding rename:
-# smart_unicode -> smart_text
-# force_unicode -> force_text
-try:
- from django.utils.encoding import smart_text
-except ImportError:
- from django.utils.encoding import smart_unicode as smart_text
+
+def unicode_to_repr(value):
+ # Coerce a unicode string to the correct repr return type, depending on
+ # the Python version. We wrap all our `__repr__` implementations with
+ # this and then use unicode throughout internally.
+ if six.PY2:
+ return value.encode('utf-8')
+ return value
+
+
+def unicode_http_header(value):
+ # Coerce HTTP header value to unicode.
+ if isinstance(value, six.binary_type):
+ return value.decode('iso-8859-1')
+ return value
+
+
+def total_seconds(timedelta):
+ # TimeDelta.total_seconds() is only available in Python 2.7
+ if hasattr(timedelta, 'total_seconds'):
+ return timedelta.total_seconds()
+ else:
+ return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0)
+
+
+# OrderedDict only available in Python 2.7.
+# This will always be the case in Django 1.7 and above, as these versions
+# no longer support Python 2.6.
+# For Django <= 1.6 and Python 2.6 fall back to SortedDict.
try:
- from django.utils.encoding import force_text
+ from collections import OrderedDict
except ImportError:
- from django.utils.encoding import force_unicode as force_text
+ from django.utils.datastructures import SortedDict as OrderedDict
# HttpResponseBase only exists from 1.5 onwards
@@ -41,437 +65,163 @@ try:
except ImportError:
from django.http import HttpResponse as HttpResponseBase
-# django-filter is optional
-try:
- import django_filters
-except ImportError:
- django_filters = None
-
-# cStringIO only if it's available, otherwise StringIO
+# contrib.postgres only supported from 1.8 onwards.
try:
- import cStringIO.StringIO as StringIO
+ from django.contrib.postgres import fields as postgres_fields
except ImportError:
- StringIO = six.StringIO
-
-BytesIO = six.BytesIO
+ postgres_fields = None
-# urlparse compat import (Required because it changed in python 3.x)
-try:
- from urllib import parse as urlparse
-except ImportError:
- import urlparse
+# request only provides `resolver_match` from 1.5 onwards.
+def get_resolver_match(request):
+ try:
+ return request.resolver_match
+ except AttributeError:
+ # Django < 1.5
+ from django.core.urlresolvers import resolve
+ return resolve(request.path_info)
-# Try to import PIL in either of the two ways it can end up installed.
+# django-filter is optional
try:
- from PIL import Image
+ import django_filters
except ImportError:
+ django_filters = None
+
+if django.VERSION >= (1, 6):
+ def clean_manytomany_helptext(text):
+ return text
+else:
+ # Up to version 1.5 many to many fields automatically suffix
+ # the `help_text` attribute with hardcoded text.
+ def clean_manytomany_helptext(text):
+ if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
+ text = text[:-69]
+ return text
+
+# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
+# Fixes (#1712). We keep the try/except for the test suite.
+guardian = None
+if 'guardian' in settings.INSTALLED_APPS:
try:
- import Image
+ import guardian
+ import guardian.shortcuts # Fixes #1624
except ImportError:
- Image = None
+ pass
-def get_concrete_model(model_cls):
+def get_model_name(model_cls):
try:
- return model_cls._meta.concrete_model
+ return model_cls._meta.model_name
except AttributeError:
- # 1.3 does not include concrete model
- return model_cls
-
-
-# Django 1.5 add support for custom auth user model
-if django.VERSION >= (1, 5):
- AUTH_USER_MODEL = settings.AUTH_USER_MODEL
-else:
- AUTH_USER_MODEL = 'auth.User'
+ # < 1.6 used module_name instead of model_name
+ return model_cls._meta.module_name
+# View._allowed_methods only present from 1.5 onwards
if django.VERSION >= (1, 5):
from django.views.generic import View
else:
- from django.views.generic import View as _View
- from django.utils.decorators import classonlymethod
- from django.utils.functional import update_wrapper
-
- class View(_View):
- # 1.3 does not include head method in base View class
- # See: https://code.djangoproject.com/ticket/15668
- @classonlymethod
- def as_view(cls, **initkwargs):
- """
- Main entry point for a request-response process.
- """
- # sanitize keyword arguments
- for key in initkwargs:
- if key in cls.http_method_names:
- raise TypeError("You tried to pass in the %s method name as a "
- "keyword argument to %s(). Don't do that."
- % (key, cls.__name__))
- if not hasattr(cls, key):
- raise TypeError("%s() received an invalid keyword %r" % (
- cls.__name__, key))
-
- def view(request, *args, **kwargs):
- self = cls(**initkwargs)
- if hasattr(self, 'get') and not hasattr(self, 'head'):
- self.head = self.get
- return self.dispatch(request, *args, **kwargs)
-
- # take name and docstring from class
- update_wrapper(view, cls, updated=())
-
- # and possible attributes set by decorators
- # like csrf_exempt from dispatch
- update_wrapper(view, cls.dispatch, assigned=())
- return view
-
- # _allowed_methods only present from 1.5 onwards
+ from django.views.generic import View as DjangoView
+
+ class View(DjangoView):
def _allowed_methods(self):
return [m.upper() for m in self.http_method_names if hasattr(self, m)]
-# PATCH method is not implemented by Django
-if 'patch' not in View.http_method_names:
- View.http_method_names = View.http_method_names + ['patch']
-
-
-# PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
-if django.VERSION >= (1, 4):
- from django.middleware.csrf import CsrfViewMiddleware
+# MinValueValidator, MaxValueValidator et al. only accept `message` in 1.8+
+if django.VERSION >= (1, 8):
+ from django.core.validators import MinValueValidator, MaxValueValidator
+ from django.core.validators import MinLengthValidator, MaxLengthValidator
else:
- import hashlib
- import re
- import random
- import logging
-
- from django.conf import settings
- from django.core.urlresolvers import get_callable
-
- try:
- from logging import NullHandler
- except ImportError:
- class NullHandler(logging.Handler):
- def emit(self, record):
- pass
-
- logger = logging.getLogger('django.request')
-
- if not logger.handlers:
- logger.addHandler(NullHandler())
-
- def same_origin(url1, url2):
- """
- Checks if two URLs are 'same-origin'
- """
- p1, p2 = urlparse.urlparse(url1), urlparse.urlparse(url2)
- return p1[0:2] == p2[0:2]
-
- def constant_time_compare(val1, val2):
- """
- Returns True if the two strings are equal, False otherwise.
-
- The time taken is independent of the number of characters that match.
- """
- if len(val1) != len(val2):
- return False
- result = 0
- for x, y in zip(val1, val2):
- result |= ord(x) ^ ord(y)
- return result == 0
-
- # Use the system (hardware-based) random number generator if it exists.
- if hasattr(random, 'SystemRandom'):
- randrange = random.SystemRandom().randrange
- else:
- randrange = random.randrange
-
- _MAX_CSRF_KEY = 18446744073709551616 # 2 << 63
-
- REASON_NO_REFERER = "Referer checking failed - no Referer."
- REASON_BAD_REFERER = "Referer checking failed - %s does not match %s."
- REASON_NO_CSRF_COOKIE = "CSRF cookie not set."
- REASON_BAD_TOKEN = "CSRF token missing or incorrect."
-
- def _get_failure_view():
- """
- Returns the view to be used for CSRF rejections
- """
- return get_callable(settings.CSRF_FAILURE_VIEW)
-
- def _get_new_csrf_key():
- return hashlib.md5("%s%s" % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest()
-
- def get_token(request):
- """
- Returns the the CSRF token required for a POST form. The token is an
- alphanumeric value.
-
- A side effect of calling this function is to make the the csrf_protect
- decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie'
- header to the outgoing response. For this reason, you may need to use this
- function lazily, as is done by the csrf context processor.
- """
- request.META["CSRF_COOKIE_USED"] = True
- return request.META.get("CSRF_COOKIE", None)
-
- def _sanitize_token(token):
- # Allow only alphanum, and ensure we return a 'str' for the sake of the post
- # processing middleware.
- token = re.sub('[^a-zA-Z0-9]', '', str(token.decode('ascii', 'ignore')))
- if token == "":
- # In case the cookie has been truncated to nothing at some point.
- return _get_new_csrf_key()
- else:
- return token
-
- class CsrfViewMiddleware(object):
- """
- Middleware that requires a present and correct csrfmiddlewaretoken
- for POST requests that have a CSRF cookie, and sets an outgoing
- CSRF cookie.
+ from django.core.validators import MinValueValidator as DjangoMinValueValidator
+ from django.core.validators import MaxValueValidator as DjangoMaxValueValidator
+ from django.core.validators import MinLengthValidator as DjangoMinLengthValidator
+ from django.core.validators import MaxLengthValidator as DjangoMaxLengthValidator
+
+ class MinValueValidator(DjangoMinValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MinValueValidator, self).__init__(*args, **kwargs)
+
+ class MaxValueValidator(DjangoMaxValueValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MaxValueValidator, self).__init__(*args, **kwargs)
+
+ class MinLengthValidator(DjangoMinLengthValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MinLengthValidator, self).__init__(*args, **kwargs)
+
+ class MaxLengthValidator(DjangoMaxLengthValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(MaxLengthValidator, self).__init__(*args, **kwargs)
+
+
+# URLValidator only accepts `message` in 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import URLValidator
+else:
+ from django.core.validators import URLValidator as DjangoURLValidator
- This middleware should be used in conjunction with the csrf_token template
- tag.
- """
- # The _accept and _reject methods currently only exist for the sake of the
- # requires_csrf_token decorator.
- def _accept(self, request):
- # Avoid checking the request twice by adding a custom attribute to
- # request. This will be relevant when both decorator and middleware
- # are used.
- request.csrf_processing_done = True
- return None
-
- def _reject(self, request, reason):
- return _get_failure_view()(request, reason=reason)
-
- def process_view(self, request, callback, callback_args, callback_kwargs):
-
- if getattr(request, 'csrf_processing_done', False):
- return None
-
- try:
- csrf_token = _sanitize_token(request.COOKIES[settings.CSRF_COOKIE_NAME])
- # Use same token next time
- request.META['CSRF_COOKIE'] = csrf_token
- except KeyError:
- csrf_token = None
- # Generate token and store it in the request, so it's available to the view.
- request.META["CSRF_COOKIE"] = _get_new_csrf_key()
-
- # Wait until request.META["CSRF_COOKIE"] has been manipulated before
- # bailing out, so that get_token still works
- if getattr(callback, 'csrf_exempt', False):
- return None
-
- # Assume that anything not defined as 'safe' by RC2616 needs protection.
- if request.method not in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
- if getattr(request, '_dont_enforce_csrf_checks', False):
- # Mechanism to turn off CSRF checks for test suite. It comes after
- # the creation of CSRF cookies, so that everything else continues to
- # work exactly the same (e.g. cookies are sent etc), but before the
- # any branches that call reject()
- return self._accept(request)
-
- if request.is_secure():
- # Suppose user visits http://example.com/
- # An active network attacker,(man-in-the-middle, MITM) sends a
- # POST form which targets https://example.com/detonate-bomb/ and
- # submits it via javascript.
- #
- # The attacker will need to provide a CSRF cookie and token, but
- # that is no problem for a MITM and the session independent
- # nonce we are using. So the MITM can circumvent the CSRF
- # protection. This is true for any HTTP connection, but anyone
- # using HTTPS expects better! For this reason, for
- # https://example.com/ we need additional protection that treats
- # http://example.com/ as completely untrusted. Under HTTPS,
- # Barth et al. found that the Referer header is missing for
- # same-domain requests in only about 0.2% of cases or less, so
- # we can use strict Referer checking.
- referer = request.META.get('HTTP_REFERER')
- if referer is None:
- logger.warning('Forbidden (%s): %s' % (REASON_NO_REFERER, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_NO_REFERER)
-
- # Note that request.get_host() includes the port
- good_referer = 'https://%s/' % request.get_host()
- if not same_origin(referer, good_referer):
- reason = REASON_BAD_REFERER % (referer, good_referer)
- logger.warning('Forbidden (%s): %s' % (reason, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, reason)
-
- if csrf_token is None:
- # No CSRF cookie. For POST requests, we insist on a CSRF cookie,
- # and in this way we can avoid all CSRF attacks, including login
- # CSRF.
- logger.warning('Forbidden (%s): %s' % (REASON_NO_CSRF_COOKIE, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_NO_CSRF_COOKIE)
-
- # check non-cookie token for match
- request_csrf_token = ""
- if request.method == "POST":
- request_csrf_token = request.POST.get('csrfmiddlewaretoken', '')
-
- if request_csrf_token == "":
- # Fall back to X-CSRFToken, to make things easier for AJAX,
- # and possible for PUT/DELETE
- request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
-
- if not constant_time_compare(request_csrf_token, csrf_token):
- logger.warning('Forbidden (%s): %s' % (REASON_BAD_TOKEN, request.path),
- extra={
- 'status_code': 403,
- 'request': request,
- }
- )
- return self._reject(request, REASON_BAD_TOKEN)
-
- return self._accept(request)
-
-# timezone support is new in Django 1.4
-try:
- from django.utils import timezone
-except ImportError:
- timezone = None
+ class URLValidator(DjangoURLValidator):
+ def __init__(self, *args, **kwargs):
+ self.message = kwargs.pop('message', self.message)
+ super(URLValidator, self).__init__(*args, **kwargs)
-# dateparse is ALSO new in Django 1.4
-try:
- from django.utils.dateparse import parse_date, parse_datetime, parse_time
-except ImportError:
- import datetime
- import re
-
- date_re = re.compile(
- r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
- )
-
- datetime_re = re.compile(
- r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
- r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
- r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
- r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$'
- )
-
- time_re = re.compile(
- r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
- r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
- )
-
- def parse_date(value):
- match = date_re.match(value)
- if match:
- kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
- return datetime.date(**kw)
-
- def parse_time(value):
- match = time_re.match(value)
- if match:
- kw = match.groupdict()
- if kw['microsecond']:
- kw['microsecond'] = kw['microsecond'].ljust(6, '0')
- kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
- return datetime.time(**kw)
-
- def parse_datetime(value):
- """Parse datetime, but w/o the timezone awareness in 1.4"""
- match = datetime_re.match(value)
- if match:
- kw = match.groupdict()
- if kw['microsecond']:
- kw['microsecond'] = kw['microsecond'].ljust(6, '0')
- kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
- return datetime.datetime(**kw)
-
-
-# smart_urlquote is new on Django 1.4
-try:
- from django.utils.html import smart_urlquote
-except ImportError:
- import re
- from django.utils.encoding import smart_str
- try:
- from urllib.parse import quote, urlsplit, urlunsplit
- except ImportError: # Python 2
- from urllib import quote
- from urlparse import urlsplit, urlunsplit
-
- unquoted_percents_re = re.compile(r'%(?![0-9A-Fa-f]{2})')
- def smart_urlquote(url):
- "Quotes a URL if it isn't already quoted."
- # Handle IDN before quoting.
- scheme, netloc, path, query, fragment = urlsplit(url)
- try:
- netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
- except UnicodeError: # invalid domain part
- pass
- else:
- url = urlunsplit((scheme, netloc, path, query, fragment))
+# EmailValidator requires explicit regex prior to 1.6+
+if django.VERSION >= (1, 6):
+ from django.core.validators import EmailValidator
+else:
+ from django.core.validators import EmailValidator as DjangoEmailValidator
+ from django.core.validators import email_re
- # An URL is considered unquoted if it contains no % characters or
- # contains a % not followed by two hexadecimal digits. See #9655.
- if '%' not in url or unquoted_percents_re.search(url):
- # See http://bugs.python.org/issue2637
- url = quote(smart_str(url), safe=b'!*\'();:@&=+$,/?#[]~')
+ class EmailValidator(DjangoEmailValidator):
+ def __init__(self, *args, **kwargs):
+ super(EmailValidator, self).__init__(email_re, *args, **kwargs)
- return force_text(url)
+# PATCH method is not implemented by Django
+if 'patch' not in View.http_method_names:
+ View.http_method_names = View.http_method_names + ['patch']
-# RequestFactory only provide `generic` from 1.5 onwards
+# RequestFactory only provides `generic` from 1.5 onwards
from django.test.client import RequestFactory as DjangoRequestFactory
from django.test.client import FakePayload
+
try:
# In 1.5 the test client uses force_bytes
- from django.utils.encoding import force_bytes_or_smart_bytes
+ from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes
except ImportError:
- # In 1.3 and 1.4 the test client just uses smart_str
+ # In 1.4 the test client just uses smart_str
from django.utils.encoding import smart_str as force_bytes_or_smart_bytes
class RequestFactory(DjangoRequestFactory):
def generic(self, method, path,
data='', content_type='application/octet-stream', **extra):
- parsed = urlparse.urlparse(path)
+ parsed = _urlparse(path)
data = force_bytes_or_smart_bytes(data, settings.DEFAULT_CHARSET)
r = {
- 'PATH_INFO': self._get_path(parsed),
- 'QUERY_STRING': force_text(parsed[4]),
- 'REQUEST_METHOD': str(method),
+ 'PATH_INFO': self._get_path(parsed),
+ 'QUERY_STRING': force_text(parsed[4]),
+ 'REQUEST_METHOD': six.text_type(method),
}
if data:
r.update({
'CONTENT_LENGTH': len(data),
- 'CONTENT_TYPE': str(content_type),
- 'wsgi.input': FakePayload(data),
- })
- elif django.VERSION <= (1, 4):
- # For 1.3 we need an empty WSGI payload
- r.update({
- 'wsgi.input': FakePayload('')
+ 'CONTENT_TYPE': six.text_type(content_type),
+ 'wsgi.input': FakePayload(data),
})
r.update(extra)
return self.request(**r)
+
# Markdown is optional
try:
import markdown
@@ -486,72 +236,17 @@ try:
safe_mode = False
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text)
-
except ImportError:
apply_markdown = None
-# Yaml is optional
-try:
- import yaml
-except ImportError:
- yaml = None
-
-
-# XML is optional
-try:
- import defusedxml.ElementTree as etree
-except ImportError:
- etree = None
-
-# OAuth is optional
-try:
- # Note: The `oauth2` package actually provides oauth1.0a support. Urg.
- import oauth2 as oauth
-except ImportError:
- oauth = None
-
-# OAuth is optional
-try:
- import oauth_provider
- from oauth_provider.store import store as oauth_provider_store
-except (ImportError, ImproperlyConfigured):
- oauth_provider = None
- oauth_provider_store = None
-
-# OAuth 2 support is optional
-try:
- import provider.oauth2 as oauth2_provider
- from provider.oauth2 import models as oauth2_provider_models
- from provider.oauth2 import forms as oauth2_provider_forms
- from provider import scope as oauth2_provider_scope
- from provider import constants as oauth2_constants
- from provider import __version__ as provider_version
- if provider_version in ('0.2.3', '0.2.4'):
- # 0.2.3 and 0.2.4 are supported version that do not support
- # timezone aware datetimes
- import datetime
- provider_now = datetime.datetime.now
- else:
- # Any other supported version does use timezone aware datetimes
- from django.utils.timezone import now as provider_now
-except ImportError:
- oauth2_provider = None
- oauth2_provider_models = None
- oauth2_provider_forms = None
- oauth2_provider_scope = None
- oauth2_constants = None
- provider_now = None
-
-# Handle lazy strings
-from django.utils.functional import Promise
-
+# `separators` argument to `json.dumps()` differs between 2.x and 3.x
+# See: http://bugs.python.org/issue22767
if six.PY3:
- def is_non_str_iterable(obj):
- if (isinstance(obj, str) or
- (isinstance(obj, Promise) and obj._delegate_text)):
- return False
- return hasattr(obj, '__iter__')
+ SHORT_SEPARATORS = (',', ':')
+ LONG_SEPARATORS = (', ', ': ')
+ INDENT_SEPARATORS = (',', ': ')
else:
- def is_non_str_iterable(obj):
- return hasattr(obj, '__iter__')
+ SHORT_SEPARATORS = (b',', b':')
+ LONG_SEPARATORS = (b', ', b': ')
+ INDENT_SEPARATORS = (b',', b': ')
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index c69756a4..21de1acf 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -3,21 +3,22 @@ The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function
-based views, as well as the `@action` and `@link` decorators, which are
+based views, as well as the `@detail_route` and `@list_route` decorators, which are
used to annotate methods on viewsets that should be included by routers.
"""
from __future__ import unicode_literals
-from rest_framework.compat import six
+from django.utils import six
from rest_framework.views import APIView
import types
-def api_view(http_method_names):
+def api_view(http_method_names=None):
"""
Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument.
"""
+ http_method_names = ['GET'] if (http_method_names is None) else http_method_names
def decorator(func):
@@ -107,23 +108,29 @@ def permission_classes(permission_classes):
return decorator
-def link(**kwargs):
+def detail_route(methods=None, **kwargs):
"""
- Used to mark a method on a ViewSet that should be routed for GET requests.
+ Used to mark a method on a ViewSet that should be routed for detail requests.
"""
+ methods = ['get'] if (methods is None) else methods
+
def decorator(func):
- func.bind_to_methods = ['get']
+ func.bind_to_methods = methods
+ func.detail = True
func.kwargs = kwargs
return func
return decorator
-def action(methods=['post'], **kwargs):
+def list_route(methods=None, **kwargs):
"""
- Used to mark a method on a ViewSet that should be routed for POST requests.
+ Used to mark a method on a ViewSet that should be routed for list requests.
"""
+ methods = ['get'] if (methods is None) else methods
+
def decorator(func):
func.bind_to_methods = methods
+ func.detail = False
func.kwargs = kwargs
return func
return decorator
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 425a7214..f954c13e 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,84 +5,148 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+from django.utils import six
+from django.utils.encoding import force_text
+from django.utils.translation import ugettext_lazy as _, ungettext
from rest_framework import status
+import math
+
+
+def _force_text_recursive(data):
+ """
+ Descend into a nested data structure, forcing any
+ lazy translation strings into plain text.
+ """
+ if isinstance(data, list):
+ return [
+ _force_text_recursive(item) for item in data
+ ]
+ elif isinstance(data, dict):
+ return dict([
+ (key, _force_text_recursive(value))
+ for key, value in data.items()
+ ])
+ return force_text(data)
class APIException(Exception):
"""
Base class for REST framework exceptions.
- Subclasses should provide `.status_code` and `.detail` properties.
+ Subclasses should provide `.status_code` and `.default_detail` properties.
"""
- pass
+ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
+ default_detail = _('A server error occurred.')
+ def __init__(self, detail=None):
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
-class ParseError(APIException):
+ def __str__(self):
+ return self.detail
+
+
+# The recommended style for using `ValidationError` is to keep it namespaced
+# under `serializers`, in order to minimize potential confusion with Django's
+# built in `ValidationError`. For example:
+#
+# from rest_framework import serializers
+# raise serializers.ValidationError('Value was invalid')
+
+class ValidationError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = 'Malformed request.'
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ def __init__(self, detail):
+ # For validation errors the 'detail' key is always required.
+ # The details should always be coerced to a list if not already.
+ if not isinstance(detail, dict) and not isinstance(detail, list):
+ detail = [detail]
+ self.detail = _force_text_recursive(detail)
+
+ def __str__(self):
+ return six.text_type(self.detail)
+
+
+class ParseError(APIException):
+ status_code = status.HTTP_400_BAD_REQUEST
+ default_detail = _('Malformed request.')
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Incorrect authentication credentials.'
-
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Authentication credentials were not provided.'
-
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = 'You do not have permission to perform this action.'
+ default_detail = _('You do not have permission to perform this action.')
- def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+
+class NotFound(APIException):
+ status_code = status.HTTP_404_NOT_FOUND
+ default_detail = _('Not found.')
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = "Method '%s' not allowed."
+ default_detail = _('Method "{method}" not allowed.')
def __init__(self, method, detail=None):
- self.detail = (detail or self.default_detail) % method
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail).format(method=method)
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = "Could not satisfy the request's Accept header"
+ default_detail = _('Could not satisfy the request Accept header.')
def __init__(self, detail=None, available_renderers=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = "Unsupported media type '%s' in request."
+ default_detail = _('Unsupported media type "{media_type}" in request.')
def __init__(self, media_type, detail=None):
- self.detail = (detail or self.default_detail) % media_type
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail).format(
+ media_type=media_type
+ )
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = "Request was throttled."
- extra_detail = "Expected available in %d second%s."
+ default_detail = _('Request was throttled.')
+ extra_detail_singular = 'Expected available in {wait} second.'
+ extra_detail_plural = 'Expected available in {wait} seconds.'
def __init__(self, wait=None, detail=None):
- import math
- self.wait = wait and math.ceil(wait) or None
- if wait is not None:
- format = detail or self.default_detail + self.extra_detail
- self.detail = format % (self.wait, self.wait != 1 and 's' or '')
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
+
+ if wait is None:
+ self.wait = None
else:
- self.detail = detail or self.default_detail
+ self.wait = math.ceil(wait)
+ self.detail += ' ' + force_text(ungettext(
+ self.extra_detail_singular.format(wait=self.wait),
+ self.extra_detail_plural.format(wait=self.wait),
+ self.wait
+ ))
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f9931887..a80862e8 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -1,31 +1,39 @@
-"""
-Serializer fields perform validation on incoming data.
-
-They are very similar to Django's form fields.
-"""
from __future__ import unicode_literals
-
-import copy
-import datetime
-import inspect
-import re
-import warnings
-from decimal import Decimal, DecimalException
-from django import forms
-from django.core import validators
-from django.core.exceptions import ValidationError
from django.conf import settings
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.forms import widgets
-from django.utils.encoding import is_protected_type
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.exceptions import ValidationError as DjangoValidationError
+from django.core.validators import RegexValidator
+from django.forms import ImageField as DjangoImageField
+from django.utils import six, timezone
+from django.utils.dateparse import parse_date, parse_datetime, parse_time
+from django.utils.encoding import is_protected_type, smart_text
from django.utils.translation import ugettext_lazy as _
-from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601
from rest_framework.compat import (
- timezone, parse_date, parse_datetime, parse_time, BytesIO, six, smart_text,
- force_text, is_non_str_iterable
+ EmailValidator, MinValueValidator, MaxValueValidator,
+ MinLengthValidator, MaxLengthValidator, URLValidator, OrderedDict,
+ unicode_repr, unicode_to_repr
)
+from rest_framework.exceptions import ValidationError
from rest_framework.settings import api_settings
+from rest_framework.utils import html, representation, humanize_datetime
+import collections
+import copy
+import datetime
+import decimal
+import inspect
+import re
+import uuid
+
+
+class empty:
+ """
+ This class is used to represent no data being provided for a given input
+ or output value.
+
+ It is required because `None` may be a valid input or output value.
+ """
+ pass
def is_simple_callable(obj):
@@ -44,615 +52,861 @@ def is_simple_callable(obj):
return len_args <= len_defaults
-def get_component(obj, attr_name):
- """
- Given an object, and an attribute name,
- return that attribute on the object.
+def get_attribute(instance, attrs):
"""
- if isinstance(obj, dict):
- val = obj.get(attr_name)
- else:
- val = getattr(obj, attr_name)
-
- if is_simple_callable(val):
- return val()
- return val
-
-
-def readable_datetime_formats(formats):
- format = ', '.join(formats).replace(ISO_8601,
- 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
- return humanize_strptime(format)
-
-
-def readable_date_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
- return humanize_strptime(format)
-
-
-def readable_time_formats(formats):
- format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
- return humanize_strptime(format)
-
-
-def humanize_strptime(format_string):
- # Note that we're missing some of the locale specific mappings that
- # don't really make sense.
- mapping = {
- "%Y": "YYYY",
- "%y": "YY",
- "%m": "MM",
- "%b": "[Jan-Dec]",
- "%B": "[January-December]",
- "%d": "DD",
- "%H": "hh",
- "%I": "hh", # Requires '%p' to differentiate from '%H'.
- "%M": "mm",
- "%S": "ss",
- "%f": "uuuuuu",
- "%a": "[Mon-Sun]",
- "%A": "[Monday-Sunday]",
- "%p": "[AM|PM]",
- "%z": "[+HHMM|-HHMM]"
- }
- for key, val in mapping.items():
- format_string = format_string.replace(key, val)
- return format_string
-
+ Similar to Python's built in `getattr(instance, attr)`,
+ but takes a list of nested attributes, instead of a single attribute.
-def strip_multiple_choice_msg(help_text):
+ Also accepts either attribute lookup on objects or dictionary lookups.
"""
- Remove the 'Hold down "control" ...' message that is Django enforces in
- select multiple fields on ModelForms. (Required for 1.5 and earlier)
+ for attr in attrs:
+ if instance is None:
+ # Break out early if we get `None` at any point in a nested lookup.
+ return None
+ try:
+ if isinstance(instance, collections.Mapping):
+ instance = instance[attr]
+ else:
+ instance = getattr(instance, attr)
+ except ObjectDoesNotExist:
+ return None
+ if is_simple_callable(instance):
+ try:
+ instance = instance()
+ except (AttributeError, KeyError) as exc:
+ # If we raised an Attribute or KeyError here it'd get treated
+ # as an omitted field in `Field.get_attribute()`. Instead we
+ # raise a ValueError to ensure the exception is not masked.
+ raise ValueError('Exception raised in callable attribute "{0}"; original exception was: {1}'.format(attr, exc))
- See https://code.djangoproject.com/ticket/9321
- """
- multiple_choice_msg = _(' Hold down "Control", or "Command" on a Mac, to select more than one.')
- multiple_choice_msg = force_text(multiple_choice_msg)
+ return instance
- return help_text.replace(multiple_choice_msg, '')
+def set_value(dictionary, keys, value):
+ """
+ Similar to Python's built in `dictionary[key] = value`,
+ but takes a list of nested keys instead of a single key.
-class Field(object):
- read_only = True
- creation_counter = 0
- empty = ''
- type_name = None
- partial = False
- use_files = False
- form_field_class = forms.CharField
- type_label = 'field'
-
- def __init__(self, source=None, label=None, help_text=None):
- self.parent = None
+ set_value({'a': 1}, [], {'b': 2}) -> {'a': 1, 'b': 2}
+ set_value({'a': 1}, ['x'], 2) -> {'a': 1, 'x': 2}
+ set_value({'a': 1}, ['x', 'y'], 2) -> {'a': 1, 'x': {'y': 2}}
+ """
+ if not keys:
+ dictionary.update(value)
+ return
- self.creation_counter = Field.creation_counter
- Field.creation_counter += 1
+ for key in keys[:-1]:
+ if key not in dictionary:
+ dictionary[key] = {}
+ dictionary = dictionary[key]
- self.source = source
+ dictionary[keys[-1]] = value
- if label is not None:
- self.label = smart_text(label)
- if help_text is not None:
- self.help_text = strip_multiple_choice_msg(smart_text(help_text))
+class CreateOnlyDefault(object):
+ """
+ This class may be used to provide default values that are only used
+ for create operations, but that do not return any value for update
+ operations.
+ """
+ def __init__(self, default):
+ self.default = default
- def initialize(self, parent, field_name):
- """
- Called to set up a field prior to field_to_native or field_from_native.
+ def set_context(self, serializer_field):
+ self.is_update = serializer_field.parent.instance is not None
+ if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_update:
+ self.default.set_context(serializer_field)
- parent - The parent serializer.
- model_field - The model field this field corresponds to, if one exists.
- """
- self.parent = parent
- self.root = parent.root or parent
- self.context = self.root.context
- self.partial = self.root.partial
- if self.partial:
- self.required = False
+ def __call__(self):
+ if self.is_update:
+ raise SkipField()
+ if callable(self.default):
+ return self.default()
+ return self.default
- def field_from_native(self, data, files, field_name, into):
- """
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
- """
- return
+ def __repr__(self):
+ return unicode_to_repr(
+ '%s(%s)' % (self.__class__.__name__, unicode_repr(self.default))
+ )
- def field_to_native(self, obj, field_name):
- """
- Given and object and a field name, returns the value that should be
- serialized for that field.
- """
- if obj is None:
- return self.empty
- if self.source == '*':
- return self.to_native(obj)
+class CurrentUserDefault(object):
+ def set_context(self, serializer_field):
+ self.user = serializer_field.context['request'].user
- source = self.source or field_name
- value = obj
+ def __call__(self):
+ return self.user
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
+ def __repr__(self):
+ return unicode_to_repr('%s()' % self.__class__.__name__)
- return self.to_native(value)
- def to_native(self, value):
- """
- Converts the field's value into it's simple representation.
- """
- if is_simple_callable(value):
- value = value()
+class SkipField(Exception):
+ pass
- if is_protected_type(value):
- return value
- elif (is_non_str_iterable(value) and
- not isinstance(value, (dict, six.string_types))):
- return [self.to_native(item) for item in value]
- elif isinstance(value, dict):
- # Make sure we preserve field ordering, if it exists
- ret = SortedDict()
- for key, val in value.items():
- ret[key] = self.to_native(val)
- return ret
- return force_text(value)
- def attributes(self):
- """
- Returns a dictionary of attributes to be used when serializing to xml.
- """
- if self.type_name:
- return {'type': self.type_name}
- return {}
+NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`'
+NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`'
+NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`'
+USE_READONLYFIELD = 'Field(read_only=True) should be ReadOnlyField'
+MISSING_ERROR_MESSAGE = (
+ 'ValidationError raised by `{class_name}`, but error key `{key}` does '
+ 'not exist in the `error_messages` dictionary.'
+)
- def metadata(self):
- metadata = SortedDict()
- metadata['type'] = self.type_label
- metadata['required'] = getattr(self, 'required', False)
- optional_attrs = ['read_only', 'label', 'help_text',
- 'min_length', 'max_length']
- for attr in optional_attrs:
- value = getattr(self, attr, None)
- if value is not None and value != '':
- metadata[attr] = force_text(value, strings_only=True)
- return metadata
+class Field(object):
+ _creation_counter = 0
-class WritableField(Field):
- """
- Base for read/write fields.
- """
- default_validators = []
default_error_messages = {
'required': _('This field is required.'),
- 'invalid': _('Invalid value.'),
+ 'null': _('This field may not be null.')
}
- widget = widgets.TextInput
- default = None
+ default_validators = []
+ default_empty_html = empty
+ initial = None
- def __init__(self, source=None, label=None, help_text=None,
- read_only=False, required=None,
- validators=[], error_messages=None, widget=None,
- default=None, blank=None):
+ def __init__(self, read_only=False, write_only=False,
+ required=None, default=empty, initial=empty, source=None,
+ label=None, help_text=None, style=None,
+ error_messages=None, validators=None, allow_null=False):
+ self._creation_counter = Field._creation_counter
+ Field._creation_counter += 1
- # 'blank' is to be deprecated in favor of 'required'
- if blank is not None:
- warnings.warn('The `blank` keyword argument is deprecated. '
- 'Use the `required` keyword argument instead.',
- DeprecationWarning, stacklevel=2)
- required = not(blank)
+ # If `required` is unset, then use `True` unless a default is provided.
+ if required is None:
+ required = default is empty and not read_only
- super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
+ # Some combinations of keyword arguments do not make sense.
+ assert not (read_only and write_only), NOT_READ_ONLY_WRITE_ONLY
+ assert not (read_only and required), NOT_READ_ONLY_REQUIRED
+ assert not (required and default is not empty), NOT_REQUIRED_DEFAULT
+ assert not (read_only and self.__class__ == Field), USE_READONLYFIELD
self.read_only = read_only
- if required is None:
- self.required = not(read_only)
- else:
- assert not (read_only and required), "Cannot set required=True and read_only=True"
- self.required = required
+ self.write_only = write_only
+ self.required = required
+ self.default = default
+ self.source = source
+ self.initial = self.initial if (initial is empty) else initial
+ self.label = label
+ self.help_text = help_text
+ self.style = {} if style is None else style
+ self.allow_null = allow_null
+
+ if self.default_empty_html is not empty:
+ if not required:
+ self.default_empty_html = empty
+ elif default is not empty:
+ self.default_empty_html = default
+
+ if validators is not None:
+ self.validators = validators[:]
+
+ # These are set up by `.bind()` when the field is added to a serializer.
+ self.field_name = None
+ self.parent = None
+ # Collect default error message from self and parent classes
messages = {}
- for c in reversed(self.__class__.__mro__):
- messages.update(getattr(c, 'default_error_messages', {}))
+ for cls in reversed(self.__class__.__mro__):
+ messages.update(getattr(cls, 'default_error_messages', {}))
messages.update(error_messages or {})
self.error_messages = messages
- self.validators = self.default_validators + validators
- self.default = default if default is not None else self.default
+ def bind(self, field_name, parent):
+ """
+ Initializes the field name and parent for the field instance.
+ Called when a field is added to the parent serializer instance.
+ """
- # Widgets are ony used for HTML forms.
- widget = widget or self.widget
- if isinstance(widget, type):
- widget = widget()
- self.widget = widget
+ # In order to enforce a consistent style, we error if a redundant
+ # 'source' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ assert self.source != field_name, (
+ "It is redundant to specify `source='%s'` on field '%s' in "
+ "serializer '%s', because it is the same as the field name. "
+ "Remove the `source` keyword argument." %
+ (field_name, self.__class__.__name__, parent.__class__.__name__)
+ )
+
+ self.field_name = field_name
+ self.parent = parent
- def __deepcopy__(self, memo):
- result = copy.copy(self)
- memo[id(self)] = result
- result.validators = self.validators[:]
- return result
+ # `self.label` should default to being based on the field name.
+ if self.label is None:
+ self.label = field_name.replace('_', ' ').capitalize()
+
+ # self.source should default to being the same as the field name.
+ if self.source is None:
+ self.source = field_name
+
+ # self.source_attrs is a list of attributes that need to be looked up
+ # when serializing the instance, or populating the validated data.
+ if self.source == '*':
+ self.source_attrs = []
+ else:
+ self.source_attrs = self.source.split('.')
+
+ # .validators is a lazily loaded property, that gets its default
+ # value from `get_validators`.
+ @property
+ def validators(self):
+ if not hasattr(self, '_validators'):
+ self._validators = self.get_validators()
+ return self._validators
+
+ @validators.setter
+ def validators(self, validators):
+ self._validators = validators
+
+ def get_validators(self):
+ return self.default_validators[:]
+
+ def get_initial(self):
+ """
+ Return a value to use when the field is being returned as a primitive
+ value, without any object instance.
+ """
+ return self.initial
+
+ def get_value(self, dictionary):
+ """
+ Given the *incoming* primitive data, return the value for this field
+ that should be validated and transformed to a native value.
+ """
+ if html.is_html_input(dictionary):
+ # HTML forms will represent empty fields as '', and cannot
+ # represent None or False values directly.
+ if self.field_name not in dictionary:
+ if getattr(self.root, 'partial', False):
+ return empty
+ return self.default_empty_html
+ ret = dictionary[self.field_name]
+ if ret == '' and self.allow_null:
+ # If the field is blank, and null is a valid value then
+ # determine if we should use null instead.
+ return '' if getattr(self, 'allow_blank', False) else None
+ return ret
+ return dictionary.get(self.field_name, empty)
+
+ def get_attribute(self, instance):
+ """
+ Given the *outgoing* object instance, return the primitive value
+ that should be used for this field.
+ """
+ try:
+ return get_attribute(instance, self.source_attrs)
+ except (KeyError, AttributeError) as exc:
+ if not self.required and self.default is empty:
+ raise SkipField()
+ msg = (
+ 'Got {exc_type} when attempting to get a value for field '
+ '`{field}` on serializer `{serializer}`.\nThe serializer '
+ 'field might be named incorrectly and not match '
+ 'any attribute or key on the `{instance}` instance.\n'
+ 'Original exception text was: {exc}.'.format(
+ exc_type=type(exc).__name__,
+ field=self.field_name,
+ serializer=self.parent.__class__.__name__,
+ instance=instance.__class__.__name__,
+ exc=exc
+ )
+ )
+ raise type(exc)(msg)
+
+ def get_default(self):
+ """
+ Return the default value to use when validating data if no input
+ is provided for this field.
+
+ If a default has not been set for this field then this will simply
+ return `empty`, indicating that no value should be set in the
+ validated data for this field.
+ """
+ if self.default is empty:
+ raise SkipField()
+ if callable(self.default):
+ if hasattr(self.default, 'set_context'):
+ self.default.set_context(self)
+ return self.default()
+ return self.default
+
+ def validate_empty_values(self, data):
+ """
+ Validate empty values, and either:
+
+ * Raise `ValidationError`, indicating invalid data.
+ * Raise `SkipField`, indicating that the field should be ignored.
+ * Return (True, data), indicating an empty value that should be
+ returned without any furhter validation being applied.
+ * Return (False, data), indicating a non-empty value, that should
+ have validation applied as normal.
+ """
+ if self.read_only:
+ return (True, self.get_default())
+
+ if data is empty:
+ if getattr(self.root, 'partial', False):
+ raise SkipField()
+ if self.required:
+ self.fail('required')
+ return (True, self.get_default())
- def validate(self, value):
- if value in validators.EMPTY_VALUES and self.required:
- raise ValidationError(self.error_messages['required'])
+ if data is None:
+ if not self.allow_null:
+ self.fail('null')
+ return (True, None)
+
+ return (False, data)
+
+ def run_validation(self, data=empty):
+ """
+ Validate a simple representation and return the internal value.
+
+ The provided data may be `empty` if no representation was included
+ in the input.
+
+ May raise `SkipField` if the field should not be included in the
+ validated data.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+ value = self.to_internal_value(data)
+ self.run_validators(value)
+ return value
def run_validators(self, value):
- if value in validators.EMPTY_VALUES:
- return
+ """
+ Test the given value against all the validators on the field,
+ and either raise a `ValidationError` or simply return.
+ """
errors = []
- for v in self.validators:
+ for validator in self.validators:
+ if hasattr(validator, 'set_context'):
+ validator.set_context(self)
+
try:
- v(value)
- except ValidationError as e:
- if hasattr(e, 'code') and e.code in self.error_messages:
- message = self.error_messages[e.code]
- if e.params:
- message = message % e.params
- errors.append(message)
- else:
- errors.extend(e.messages)
+ validator(value)
+ except ValidationError as exc:
+ # If the validation error contains a mapping of fields to
+ # errors then simply raise it immediately rather than
+ # attempting to accumulate a list of errors.
+ if isinstance(exc.detail, dict):
+ raise
+ errors.extend(exc.detail)
+ except DjangoValidationError as exc:
+ errors.extend(exc.messages)
if errors:
raise ValidationError(errors)
- def field_from_native(self, data, files, field_name, into):
+ def to_internal_value(self, data):
"""
- Given a dictionary and a field name, updates the dictionary `into`,
- with the field and it's deserialized value.
+ Transform the *incoming* primitive data into a native value.
"""
- if self.read_only:
- return
+ raise NotImplementedError(
+ '{cls}.to_internal_value() must be implemented.'.format(
+ cls=self.__class__.__name__
+ )
+ )
+ def to_representation(self, value):
+ """
+ Transform the *outgoing* native value into primitive data.
+ """
+ raise NotImplementedError(
+ '{cls}.to_representation() must be implemented.\n'
+ 'If you are upgrading from REST framework version 2 '
+ 'you might want `ReadOnlyField`.'.format(
+ cls=self.__class__.__name__
+ )
+ )
+
+ def fail(self, key, **kwargs):
+ """
+ A helper method that simply raises a validation error.
+ """
try:
- if self.use_files:
- files = files or {}
- native = files[field_name]
- else:
- native = data[field_name]
+ msg = self.error_messages[key]
except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- if is_simple_callable(self.default):
- native = self.default()
- else:
- native = self.default
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
-
- value = self.from_native(native)
- if self.source == '*':
- if value:
- into.update(value)
- else:
- self.validate(value)
- self.run_validators(value)
- into[self.source or field_name] = value
-
- def from_native(self, value):
+ class_name = self.__class__.__name__
+ msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
+ raise AssertionError(msg)
+ message_string = msg.format(**kwargs)
+ raise ValidationError(message_string)
+
+ @property
+ def root(self):
"""
- Reverts a simple representation back to the field's value.
+ Returns the top-level serializer for this field.
"""
- return value
+ root = self
+ while root.parent is not None:
+ root = root.parent
+ return root
+ @property
+ def context(self):
+ """
+ Returns the context as passed to the root serializer on initialization.
+ """
+ return getattr(self.root, '_context', {})
-class ModelField(WritableField):
- """
- A generic field that can be used against an arbitrary model field.
- """
- def __init__(self, *args, **kwargs):
- try:
- self.model_field = kwargs.pop('model_field')
- except KeyError:
- raise ValueError("ModelField requires 'model_field' kwarg")
+ def __new__(cls, *args, **kwargs):
+ """
+ When a field is instantiated, we store the arguments that were used,
+ so that we can present a helpful representation of the object.
+ """
+ instance = super(Field, cls).__new__(cls)
+ instance._args = args
+ instance._kwargs = kwargs
+ return instance
- self.min_length = kwargs.pop('min_length',
- getattr(self.model_field, 'min_length', None))
- self.max_length = kwargs.pop('max_length',
- getattr(self.model_field, 'max_length', None))
- self.min_value = kwargs.pop('min_value',
- getattr(self.model_field, 'min_value', None))
- self.max_value = kwargs.pop('max_value',
- getattr(self.model_field, 'max_value', None))
+ def __deepcopy__(self, memo):
+ """
+ When cloning fields we instantiate using the arguments it was
+ originally created with, rather than copying the complete state.
+ """
+ args = copy.deepcopy(self._args)
+ kwargs = dict(self._kwargs)
+ # Bit ugly, but we need to special case 'validators' as Django's
+ # RegexValidator does not support deepcopy.
+ # We treat validator callables as immutable objects.
+ # See https://github.com/tomchristie/django-rest-framework/issues/1954
+ validators = kwargs.pop('validators', None)
+ kwargs = copy.deepcopy(kwargs)
+ if validators is not None:
+ kwargs['validators'] = validators
+ return self.__class__(*args, **kwargs)
+
+ def __repr__(self):
+ """
+ Fields are represented using their initial calling arguments.
+ This allows us to create descriptive representations for serializer
+ instances that show all the declared fields on the serializer.
+ """
+ return unicode_to_repr(representation.field_repr(self))
- super(ModelField, self).__init__(*args, **kwargs)
- if self.min_length is not None:
- self.validators.append(validators.MinLengthValidator(self.min_length))
- if self.max_length is not None:
- self.validators.append(validators.MaxLengthValidator(self.max_length))
- if self.min_value is not None:
- self.validators.append(validators.MinValueValidator(self.min_value))
- if self.max_value is not None:
- self.validators.append(validators.MaxValueValidator(self.max_value))
+# Boolean types...
- def from_native(self, value):
- rel = getattr(self.model_field, "rel", None)
- if rel is not None:
- return rel.to._meta.get_field(rel.field_name).to_python(value)
- else:
- return self.model_field.to_python(value)
+class BooleanField(Field):
+ default_error_messages = {
+ 'invalid': _('"{input}" is not a valid boolean.')
+ }
+ default_empty_html = False
+ initial = False
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
- def field_to_native(self, obj, field_name):
- value = self.model_field._get_val_from_obj(obj)
- if is_protected_type(value):
- return value
- return self.model_field.value_to_string(obj)
+ def __init__(self, **kwargs):
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
+ super(BooleanField, self).__init__(**kwargs)
- def attributes(self):
- return {
- "type": self.model_field.get_internal_type()
- }
+ def to_internal_value(self, data):
+ if data in self.TRUE_VALUES:
+ return True
+ elif data in self.FALSE_VALUES:
+ return False
+ self.fail('invalid', input=data)
+ def to_representation(self, value):
+ if value in self.TRUE_VALUES:
+ return True
+ elif value in self.FALSE_VALUES:
+ return False
+ return bool(value)
-##### Typed Fields #####
-class BooleanField(WritableField):
- type_name = 'BooleanField'
- type_label = 'boolean'
- form_field_class = forms.BooleanField
- widget = widgets.CheckboxInput
+class NullBooleanField(Field):
default_error_messages = {
- 'invalid': _("'%s' value must be either True or False."),
+ 'invalid': _('"{input}" is not a valid boolean.')
}
- empty = False
+ initial = None
+ TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True))
+ FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False))
+ NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None))
+
+ def __init__(self, **kwargs):
+ assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
+ kwargs['allow_null'] = True
+ super(NullBooleanField, self).__init__(**kwargs)
- # Note: we set default to `False` in order to fill in missing value not
- # supplied by html form. TODO: Fix so that only html form input gets
- # this behavior.
- default = False
+ def to_internal_value(self, data):
+ if data in self.TRUE_VALUES:
+ return True
+ elif data in self.FALSE_VALUES:
+ return False
+ elif data in self.NULL_VALUES:
+ return None
+ self.fail('invalid', input=data)
- def from_native(self, value):
- if value in ('true', 't', 'True', '1'):
+ def to_representation(self, value):
+ if value in self.NULL_VALUES:
+ return None
+ if value in self.TRUE_VALUES:
return True
- if value in ('false', 'f', 'False', '0'):
+ elif value in self.FALSE_VALUES:
return False
return bool(value)
-class CharField(WritableField):
- type_name = 'CharField'
- type_label = 'string'
- form_field_class = forms.CharField
+# String types...
- def __init__(self, max_length=None, min_length=None, *args, **kwargs):
- self.max_length, self.min_length = max_length, min_length
- super(CharField, self).__init__(*args, **kwargs)
- if min_length is not None:
- self.validators.append(validators.MinLengthValidator(min_length))
- if max_length is not None:
- self.validators.append(validators.MaxLengthValidator(max_length))
+class CharField(Field):
+ default_error_messages = {
+ 'blank': _('This field may not be blank.'),
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ 'min_length': _('Ensure this field has at least {min_length} characters.')
+ }
+ initial = ''
- def from_native(self, value):
- if isinstance(value, six.string_types) or value is None:
- return value
- return smart_text(value)
+ def __init__(self, **kwargs):
+ self.allow_blank = kwargs.pop('allow_blank', False)
+ self.trim_whitespace = kwargs.pop('trim_whitespace', True)
+ self.max_length = kwargs.pop('max_length', None)
+ self.min_length = kwargs.pop('min_length', None)
+ super(CharField, self).__init__(**kwargs)
+ if self.max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=self.max_length)
+ self.validators.append(MaxLengthValidator(self.max_length, message=message))
+ if self.min_length is not None:
+ message = self.error_messages['min_length'].format(min_length=self.min_length)
+ self.validators.append(MinLengthValidator(self.min_length, message=message))
+ def run_validation(self, data=empty):
+ # Test for the empty string here so that it does not get validated,
+ # and so that subclasses do not need to handle it explicitly
+ # inside the `to_internal_value()` method.
+ if data == '':
+ if not self.allow_blank:
+ self.fail('blank')
+ return ''
+ return super(CharField, self).run_validation(data)
-class URLField(CharField):
- type_name = 'URLField'
- type_label = 'url'
+ def to_internal_value(self, data):
+ value = six.text_type(data)
+ return value.strip() if self.trim_whitespace else value
+
+ def to_representation(self, value):
+ return six.text_type(value)
+
+
+class EmailField(CharField):
+ default_error_messages = {
+ 'invalid': _('Enter a valid email address.')
+ }
def __init__(self, **kwargs):
- kwargs['validators'] = [validators.URLValidator()]
- super(URLField, self).__init__(**kwargs)
+ super(EmailField, self).__init__(**kwargs)
+ validator = EmailValidator(message=self.error_messages['invalid'])
+ self.validators.append(validator)
+
+
+class RegexField(CharField):
+ default_error_messages = {
+ 'invalid': _('This value does not match the required pattern.')
+ }
+
+ def __init__(self, regex, **kwargs):
+ super(RegexField, self).__init__(**kwargs)
+ validator = RegexValidator(regex, message=self.error_messages['invalid'])
+ self.validators.append(validator)
class SlugField(CharField):
- type_name = 'SlugField'
- type_label = 'slug'
- form_field_class = forms.SlugField
+ default_error_messages = {
+ 'invalid': _('Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.')
+ }
+ def __init__(self, **kwargs):
+ super(SlugField, self).__init__(**kwargs)
+ slug_regex = re.compile(r'^[-a-zA-Z0-9_]+$')
+ validator = RegexValidator(slug_regex, message=self.error_messages['invalid'])
+ self.validators.append(validator)
+
+
+class URLField(CharField):
default_error_messages = {
- 'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
- " underscores or hyphens."),
+ 'invalid': _('Enter a valid URL.')
}
- default_validators = [validators.validate_slug]
- def __init__(self, *args, **kwargs):
- super(SlugField, self).__init__(*args, **kwargs)
+ def __init__(self, **kwargs):
+ super(URLField, self).__init__(**kwargs)
+ validator = URLValidator(message=self.error_messages['invalid'])
+ self.validators.append(validator)
-class ChoiceField(WritableField):
- type_name = 'ChoiceField'
- type_label = 'multiple choice'
- form_field_class = forms.ChoiceField
- widget = widgets.Select
+class UUIDField(Field):
default_error_messages = {
- 'invalid_choice': _('Select a valid choice. %(value)s is not one of '
- 'the available choices.'),
+ 'invalid': _('"{value}" is not a valid UUID.'),
}
- def __init__(self, choices=(), *args, **kwargs):
- super(ChoiceField, self).__init__(*args, **kwargs)
- self.choices = choices
- if not self.required:
- self.choices = BLANK_CHOICE_DASH + self.choices
+ def to_internal_value(self, data):
+ if not isinstance(data, uuid.UUID):
+ try:
+ return uuid.UUID(data)
+ except (ValueError, TypeError):
+ self.fail('invalid', value=data)
+ return data
- def _get_choices(self):
- return self._choices
+ def to_representation(self, value):
+ return str(value)
- def _set_choices(self, value):
- # Setting choices also sets the choices on the widget.
- # choices can be any iterable, but we call list() on it because
- # it will be consumed more than once.
- self._choices = self.widget.choices = list(value)
- choices = property(_get_choices, _set_choices)
+# Number types...
- def validate(self, value):
- """
- Validates that the input is in self.choices.
- """
- super(ChoiceField, self).validate(value)
- if value and not self.valid_value(value):
- raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
+class IntegerField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid integer is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
+ }
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
- def valid_value(self, value):
- """
- Check to see if the provided value is a valid choice.
- """
- for k, v in self.choices:
- if isinstance(v, (list, tuple)):
- # This is an optgroup, so look inside the group for options
- for k2, v2 in v:
- if value == smart_text(k2):
- return True
- else:
- if value == smart_text(k) or value == k:
- return True
- return False
+ def __init__(self, **kwargs):
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
+ super(IntegerField, self).__init__(**kwargs)
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+ def to_internal_value(self, data):
+ if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
-class EmailField(CharField):
- type_name = 'EmailField'
- type_label = 'email'
- form_field_class = forms.EmailField
+ try:
+ data = int(data)
+ except (ValueError, TypeError):
+ self.fail('invalid')
+ return data
+
+ def to_representation(self, value):
+ return int(value)
+
+class FloatField(Field):
default_error_messages = {
- 'invalid': _('Enter a valid email address.'),
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_string_length': _('String value too large.')
}
- default_validators = [validators.validate_email]
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
- def from_native(self, value):
- ret = super(EmailField, self).from_native(value)
- if ret is None:
- return None
- return ret.strip()
+ def __init__(self, **kwargs):
+ self.max_value = kwargs.pop('max_value', None)
+ self.min_value = kwargs.pop('min_value', None)
+ super(FloatField, self).__init__(**kwargs)
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+ def to_internal_value(self, data):
+ if isinstance(data, six.text_type) and len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
-class RegexField(CharField):
- type_name = 'RegexField'
- type_label = 'regex'
- form_field_class = forms.RegexField
+ try:
+ return float(data)
+ except (TypeError, ValueError):
+ self.fail('invalid')
+
+ def to_representation(self, value):
+ return float(value)
+
+
+class DecimalField(Field):
+ default_error_messages = {
+ 'invalid': _('A valid number is required.'),
+ 'max_value': _('Ensure this value is less than or equal to {max_value}.'),
+ 'min_value': _('Ensure this value is greater than or equal to {min_value}.'),
+ 'max_digits': _('Ensure that there are no more than {max_digits} digits in total.'),
+ 'max_decimal_places': _('Ensure that there are no more than {max_decimal_places} decimal places.'),
+ 'max_whole_digits': _('Ensure that there are no more than {max_whole_digits} digits before the decimal point.'),
+ 'max_string_length': _('String value too large.')
+ }
+ MAX_STRING_LENGTH = 1000 # Guard against malicious string inputs.
+
+ coerce_to_string = api_settings.COERCE_DECIMAL_TO_STRING
+
+ def __init__(self, max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None, **kwargs):
+ self.max_digits = max_digits
+ self.decimal_places = decimal_places
+ self.coerce_to_string = coerce_to_string if (coerce_to_string is not None) else self.coerce_to_string
+
+ self.max_value = max_value
+ self.min_value = min_value
+
+ super(DecimalField, self).__init__(**kwargs)
+
+ if self.max_value is not None:
+ message = self.error_messages['max_value'].format(max_value=self.max_value)
+ self.validators.append(MaxValueValidator(self.max_value, message=message))
+ if self.min_value is not None:
+ message = self.error_messages['min_value'].format(min_value=self.min_value)
+ self.validators.append(MinValueValidator(self.min_value, message=message))
+
+ def to_internal_value(self, data):
+ """
+ Validates that the input is a decimal number. Returns a Decimal
+ instance. Returns None for empty values. Ensures that there are no more
+ than max_digits in the number, and no more than decimal_places digits
+ after the decimal point.
+ """
+ data = smart_text(data).strip()
+ if len(data) > self.MAX_STRING_LENGTH:
+ self.fail('max_string_length')
- def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
- super(RegexField, self).__init__(max_length, min_length, *args, **kwargs)
- self.regex = regex
+ try:
+ value = decimal.Decimal(data)
+ except decimal.DecimalException:
+ self.fail('invalid')
- def _get_regex(self):
- return self._regex
+ # Check for NaN. It is the only value that isn't equal to itself,
+ # so we can use this to identify NaN values.
+ if value != value:
+ self.fail('invalid')
- def _set_regex(self, regex):
- if isinstance(regex, six.string_types):
- regex = re.compile(regex)
- self._regex = regex
- if hasattr(self, '_regex_validator') and self._regex_validator in self.validators:
- self.validators.remove(self._regex_validator)
- self._regex_validator = validators.RegexValidator(regex=regex)
- self.validators.append(self._regex_validator)
+ # Check for infinity and negative infinity.
+ if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')):
+ self.fail('invalid')
- regex = property(_get_regex, _set_regex)
+ sign, digittuple, exponent = value.as_tuple()
+ decimals = abs(exponent)
+ # digittuple doesn't include any leading zeros.
+ digits = len(digittuple)
+ if decimals > digits:
+ # We have leading zeros up to or past the decimal point. Count
+ # everything past the decimal point as a digit. We do not count
+ # 0 before the decimal point as a digit since that would mean
+ # we would not allow max_digits = decimal_places.
+ digits = decimals
+ whole_digits = digits - decimals
+
+ if self.max_digits is not None and digits > self.max_digits:
+ self.fail('max_digits', max_digits=self.max_digits)
+ if self.decimal_places is not None and decimals > self.decimal_places:
+ self.fail('max_decimal_places', max_decimal_places=self.decimal_places)
+ if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
+ self.fail('max_whole_digits', max_whole_digits=self.max_digits - self.decimal_places)
+
+ return value
+
+ def to_representation(self, value):
+ if not isinstance(value, decimal.Decimal):
+ value = decimal.Decimal(six.text_type(value).strip())
+
+ context = decimal.getcontext().copy()
+ context.prec = self.max_digits
+ quantized = value.quantize(
+ decimal.Decimal('.1') ** self.decimal_places,
+ context=context
+ )
+ if not self.coerce_to_string:
+ return quantized
+ return '{0:f}'.format(quantized)
-class DateField(WritableField):
- type_name = 'DateField'
- type_label = 'date'
- widget = widgets.DateInput
- form_field_class = forms.DateField
+# Date & time fields...
+class DateTimeField(Field):
default_error_messages = {
- 'invalid': _("Date has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Datetime has wrong format. Use one of these formats instead: {format}.'),
+ 'date': _('Expected a datetime but got a date.'),
}
- empty = None
- input_formats = api_settings.DATE_INPUT_FORMATS
- format = api_settings.DATE_FORMAT
+ format = api_settings.DATETIME_FORMAT
+ input_formats = api_settings.DATETIME_INPUT_FORMATS
+ default_timezone = timezone.get_default_timezone() if settings.USE_TZ else None
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, default_timezone=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateField, self).__init__(*args, **kwargs)
+ self.default_timezone = default_timezone if default_timezone is not None else self.default_timezone
+ super(DateTimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ def enforce_timezone(self, value):
+ """
+ When `self.default_timezone` is `None`, always return naive datetimes.
+ When `self.default_timezone` is not `None`, always return aware datetimes.
+ """
+ if (self.default_timezone is not None) and not timezone.is_aware(value):
+ return timezone.make_aware(value, self.default_timezone)
+ elif (self.default_timezone is None) and timezone.is_aware(value):
+ return timezone.make_naive(value, timezone.UTC())
+ return value
+
+ def to_internal_value(self, value):
+ if isinstance(value, datetime.date) and not isinstance(value, datetime.datetime):
+ self.fail('date')
if isinstance(value, datetime.datetime):
- if timezone and settings.USE_TZ and timezone.is_aware(value):
- # Convert aware datetimes to the default time zone
- # before casting them to dates (#17742).
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_naive(value, default_timezone)
- return value.date()
- if isinstance(value, datetime.date):
- return value
+ return self.enforce_timezone(value)
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
- parsed = parse_date(value)
+ parsed = parse_datetime(value)
except (ValueError, TypeError):
pass
else:
if parsed is not None:
- return parsed
+ return self.enforce_timezone(parsed)
else:
try:
parsed = datetime.datetime.strptime(value, format)
except (ValueError, TypeError):
pass
else:
- return parsed.date()
+ return self.enforce_timezone(parsed)
- msg = self.error_messages['invalid'] % readable_date_formats(self.input_formats)
- raise ValidationError(msg)
+ humanized_format = humanize_datetime.datetime_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
- def to_native(self, value):
- if value is None or self.format is None:
+ def to_representation(self, value):
+ if self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.date()
-
if self.format.lower() == ISO_8601:
- return value.isoformat()
+ value = value.isoformat()
+ if value.endswith('+00:00'):
+ value = value[:-6] + 'Z'
+ return value
return value.strftime(self.format)
-class DateTimeField(WritableField):
- type_name = 'DateTimeField'
- type_label = 'datetime'
- widget = widgets.DateTimeInput
- form_field_class = forms.DateTimeField
-
+class DateField(Field):
default_error_messages = {
- 'invalid': _("Datetime has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Date has wrong format. Use one of these formats instead: {format}.'),
+ 'datetime': _('Expected a date but got a datetime.'),
}
- empty = None
- input_formats = api_settings.DATETIME_INPUT_FORMATS
- format = api_settings.DATETIME_FORMAT
+ format = api_settings.DATE_FORMAT
+ input_formats = api_settings.DATE_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
- super(DateTimeField, self).__init__(*args, **kwargs)
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ super(DateField, self).__init__(*args, **kwargs)
+ def to_internal_value(self, value):
if isinstance(value, datetime.datetime):
- return value
+ self.fail('datetime')
+
if isinstance(value, datetime.date):
- value = datetime.datetime(value.year, value.month, value.day)
- if settings.USE_TZ:
- # For backwards compatibility, interpret naive datetimes in
- # local time. This won't work during DST change, but we can't
- # do much about it, so we let the exceptions percolate up the
- # call stack.
- warnings.warn("DateTimeField received a naive datetime (%s)"
- " while time zone support is active." % value,
- RuntimeWarning)
- default_timezone = timezone.get_default_timezone()
- value = timezone.make_aware(value, default_timezone)
return value
for format in self.input_formats:
if format.lower() == ISO_8601:
try:
- parsed = parse_datetime(value)
+ parsed = parse_date(value)
except (ValueError, TypeError):
pass
else:
@@ -664,45 +918,42 @@ class DateTimeField(WritableField):
except (ValueError, TypeError):
pass
else:
- return parsed
+ return parsed.date()
- msg = self.error_messages['invalid'] % readable_datetime_formats(self.input_formats)
- raise ValidationError(msg)
+ humanized_format = humanize_datetime.date_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
- def to_native(self, value):
- if value is None or self.format is None:
+ def to_representation(self, value):
+ if self.format is None:
return value
+ # Applying a `DateField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `date`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
+
if self.format.lower() == ISO_8601:
- ret = value.isoformat()
- if ret.endswith('+00:00'):
- ret = ret[:-6] + 'Z'
- return ret
+ return value.isoformat()
return value.strftime(self.format)
-class TimeField(WritableField):
- type_name = 'TimeField'
- type_label = 'time'
- widget = widgets.TimeInput
- form_field_class = forms.TimeField
-
+class TimeField(Field):
default_error_messages = {
- 'invalid': _("Time has wrong format. Use one of these formats instead: %s"),
+ 'invalid': _('Time has wrong format. Use one of these formats instead: {format}.'),
}
- empty = None
- input_formats = api_settings.TIME_INPUT_FORMATS
format = api_settings.TIME_FORMAT
+ input_formats = api_settings.TIME_INPUT_FORMATS
- def __init__(self, input_formats=None, format=None, *args, **kwargs):
+ def __init__(self, format=empty, input_formats=None, *args, **kwargs):
+ self.format = format if format is not empty else self.format
self.input_formats = input_formats if input_formats is not None else self.input_formats
- self.format = format if format is not None else self.format
super(TimeField, self).__init__(*args, **kwargs)
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
+ def to_internal_value(self, value):
if isinstance(value, datetime.time):
return value
@@ -723,246 +974,389 @@ class TimeField(WritableField):
else:
return parsed.time()
- msg = self.error_messages['invalid'] % readable_time_formats(self.input_formats)
- raise ValidationError(msg)
+ humanized_format = humanize_datetime.time_formats(self.input_formats)
+ self.fail('invalid', format=humanized_format)
- def to_native(self, value):
- if value is None or self.format is None:
+ def to_representation(self, value):
+ if self.format is None:
return value
- if isinstance(value, datetime.datetime):
- value = value.time()
+ # Applying a `TimeField` to a datetime value is almost always
+ # not a sensible thing to do, as it means naively dropping
+ # any explicit or implicit timezone info.
+ assert not isinstance(value, datetime.datetime), (
+ 'Expected a `time`, but got a `datetime`. Refusing to coerce, '
+ 'as this may mean losing timezone information. Use a custom '
+ 'read-only field and deal with timezone issues explicitly.'
+ )
if self.format.lower() == ISO_8601:
return value.isoformat()
return value.strftime(self.format)
-class IntegerField(WritableField):
- type_name = 'IntegerField'
- type_label = 'integer'
- form_field_class = forms.IntegerField
+# Choice types...
+class ChoiceField(Field):
default_error_messages = {
- 'invalid': _('Enter a whole number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
+ 'invalid_choice': _('"{input}" is not a valid choice.')
}
- def __init__(self, max_value=None, min_value=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- super(IntegerField, self).__init__(*args, **kwargs)
-
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
-
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
-
- try:
- value = int(str(value))
- except (ValueError, TypeError):
- raise ValidationError(self.error_messages['invalid'])
- return value
+ def __init__(self, choices, **kwargs):
+ # Allow either single or paired choices style:
+ # choices = [1, 2, 3]
+ # choices = [(1, 'First'), (2, 'Second'), (3, 'Third')]
+ pairs = [
+ isinstance(item, (list, tuple)) and len(item) == 2
+ for item in choices
+ ]
+ if all(pairs):
+ self.choices = OrderedDict([(key, display_value) for key, display_value in choices])
+ else:
+ self.choices = OrderedDict([(item, item) for item in choices])
+ # Map the string representation of choices to the underlying value.
+ # Allows us to deal with eg. integer choices while supporting either
+ # integer or string input, but still get the correct datatype out.
+ self.choice_strings_to_values = dict([
+ (six.text_type(key), key) for key in self.choices.keys()
+ ])
-class FloatField(WritableField):
- type_name = 'FloatField'
- type_label = 'float'
- form_field_class = forms.FloatField
+ self.allow_blank = kwargs.pop('allow_blank', False)
- default_error_messages = {
- 'invalid': _("'%s' value must be a float."),
- }
+ super(ChoiceField, self).__init__(**kwargs)
- def from_native(self, value):
- if value in validators.EMPTY_VALUES:
- return None
+ def to_internal_value(self, data):
+ if data == '' and self.allow_blank:
+ return ''
try:
- return float(value)
- except (TypeError, ValueError):
- msg = self.error_messages['invalid'] % value
- raise ValidationError(msg)
+ return self.choice_strings_to_values[six.text_type(data)]
+ except KeyError:
+ self.fail('invalid_choice', input=data)
+ def to_representation(self, value):
+ if value in ('', None):
+ return value
+ return self.choice_strings_to_values[six.text_type(value)]
-class DecimalField(WritableField):
- type_name = 'DecimalField'
- type_label = 'decimal'
- form_field_class = forms.DecimalField
+class MultipleChoiceField(ChoiceField):
default_error_messages = {
- 'invalid': _('Enter a number.'),
- 'max_value': _('Ensure this value is less than or equal to %(limit_value)s.'),
- 'min_value': _('Ensure this value is greater than or equal to %(limit_value)s.'),
- 'max_digits': _('Ensure that there are no more than %s digits in total.'),
- 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'),
- 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.')
+ 'invalid_choice': _('"{input}" is not a valid choice.'),
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
+ default_empty_html = []
- def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs):
- self.max_value, self.min_value = max_value, min_value
- self.max_digits, self.decimal_places = max_digits, decimal_places
- super(DecimalField, self).__init__(*args, **kwargs)
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return dictionary.getlist(self.field_name)
+ return dictionary.get(self.field_name, empty)
- if max_value is not None:
- self.validators.append(validators.MaxValueValidator(max_value))
- if min_value is not None:
- self.validators.append(validators.MinValueValidator(min_value))
+ def to_internal_value(self, data):
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
- def from_native(self, value):
- """
- Validates that the input is a decimal number. Returns a Decimal
- instance. Returns None for empty values. Ensures that there are no more
- than max_digits in the number, and no more than decimal_places digits
- after the decimal point.
- """
- if value in validators.EMPTY_VALUES:
- return None
- value = smart_text(value).strip()
- try:
- value = Decimal(value)
- except DecimalException:
- raise ValidationError(self.error_messages['invalid'])
- return value
-
- def validate(self, value):
- super(DecimalField, self).validate(value)
- if value in validators.EMPTY_VALUES:
- return
- # Check for NaN, Inf and -Inf values. We can't compare directly for NaN,
- # since it is never equal to itself. However, NaN is the only value that
- # isn't equal to itself, so we can use this to identify NaN
- if value != value or value == Decimal("Inf") or value == Decimal("-Inf"):
- raise ValidationError(self.error_messages['invalid'])
- sign, digittuple, exponent = value.as_tuple()
- decimals = abs(exponent)
- # digittuple doesn't include any leading zeros.
- digits = len(digittuple)
- if decimals > digits:
- # We have leading zeros up to or past the decimal point. Count
- # everything past the decimal point as a digit. We do not count
- # 0 before the decimal point as a digit since that would mean
- # we would not allow max_digits = decimal_places.
- digits = decimals
- whole_digits = digits - decimals
+ return set([
+ super(MultipleChoiceField, self).to_internal_value(item)
+ for item in data
+ ])
- if self.max_digits is not None and digits > self.max_digits:
- raise ValidationError(self.error_messages['max_digits'] % self.max_digits)
- if self.decimal_places is not None and decimals > self.decimal_places:
- raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places)
- if self.max_digits is not None and self.decimal_places is not None and whole_digits > (self.max_digits - self.decimal_places):
- raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places))
- return value
+ def to_representation(self, value):
+ return set([
+ self.choice_strings_to_values[six.text_type(item)] for item in value
+ ])
-class FileField(WritableField):
- use_files = True
- type_name = 'FileField'
- type_label = 'file upload'
- form_field_class = forms.FileField
- widget = widgets.FileInput
+# File types...
+class FileField(Field):
default_error_messages = {
- 'invalid': _("No file was submitted. Check the encoding type on the form."),
- 'missing': _("No file was submitted."),
- 'empty': _("The submitted file is empty."),
- 'max_length': _('Ensure this filename has at most %(max)d characters (it has %(length)d).'),
- 'contradiction': _('Please either submit a file or check the clear checkbox, not both.')
+ 'required': _('No file was submitted.'),
+ 'invalid': _('The submitted data was not a file. Check the encoding type on the form.'),
+ 'no_name': _('No filename could be determined.'),
+ 'empty': _('The submitted file is empty.'),
+ 'max_length': _('Ensure this filename has at most {max_length} characters (it has {length}).'),
}
+ use_url = api_settings.UPLOADED_FILES_USE_URL
def __init__(self, *args, **kwargs):
self.max_length = kwargs.pop('max_length', None)
self.allow_empty_file = kwargs.pop('allow_empty_file', False)
+ self.use_url = kwargs.pop('use_url', self.use_url)
super(FileField, self).__init__(*args, **kwargs)
- def from_native(self, data):
- if data in validators.EMPTY_VALUES:
- return None
-
- # UploadedFile objects should have name and size attributes.
+ def to_internal_value(self, data):
try:
+ # `UploadedFile` objects should have name and size attributes.
file_name = data.name
file_size = data.size
except AttributeError:
- raise ValidationError(self.error_messages['invalid'])
+ self.fail('invalid')
- if self.max_length is not None and len(file_name) > self.max_length:
- error_values = {'max': self.max_length, 'length': len(file_name)}
- raise ValidationError(self.error_messages['max_length'] % error_values)
if not file_name:
- raise ValidationError(self.error_messages['invalid'])
+ self.fail('no_name')
if not self.allow_empty_file and not file_size:
- raise ValidationError(self.error_messages['empty'])
+ self.fail('empty')
+ if self.max_length and len(file_name) > self.max_length:
+ self.fail('max_length', max_length=self.max_length, length=len(file_name))
return data
- def to_native(self, value):
+ def to_representation(self, value):
+ if self.use_url:
+ if not value:
+ return None
+ url = value.url
+ request = self.context.get('request', None)
+ if request is not None:
+ return request.build_absolute_uri(url)
+ return url
return value.name
class ImageField(FileField):
- use_files = True
- type_name = 'ImageField'
- type_label = 'image upload'
- form_field_class = forms.ImageField
+ default_error_messages = {
+ 'invalid_image': _(
+ 'Upload a valid image. The file you uploaded was either not an image or a corrupted image.'
+ ),
+ }
+
+ def __init__(self, *args, **kwargs):
+ self._DjangoImageField = kwargs.pop('_DjangoImageField', DjangoImageField)
+ super(ImageField, self).__init__(*args, **kwargs)
+
+ def to_internal_value(self, data):
+ # Image validation is a bit grungy, so we'll just outright
+ # defer to Django's implementation so we don't need to
+ # consider it, or treat PIL as a test dependency.
+ file_object = super(ImageField, self).to_internal_value(data)
+ django_field = self._DjangoImageField()
+ django_field.error_messages = self.error_messages
+ django_field.to_python(file_object)
+ return file_object
+
+# Composite field types...
+
+class _UnvalidatedField(Field):
+ def __init__(self, *args, **kwargs):
+ super(_UnvalidatedField, self).__init__(*args, **kwargs)
+ self.allow_blank = True
+ self.allow_null = True
+
+ def to_internal_value(self, data):
+ return data
+
+ def to_representation(self, value):
+ return value
+
+
+class ListField(Field):
+ child = _UnvalidatedField()
+ initial = []
default_error_messages = {
- 'invalid_image': _("Upload a valid image. The file you uploaded was "
- "either not an image or a corrupted image."),
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
}
- def from_native(self, data):
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListField, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
"""
- Checks that the file-upload field data contains a valid image (GIF, JPG,
- PNG, possibly others -- whatever the Python Imaging Library supports).
+ List of dicts of native values <- List of dicts of primitive datatypes.
"""
- f = super(ImageField, self).from_native(data)
- if f is None:
- return None
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
+ if isinstance(data, type('')) or not hasattr(data, '__iter__'):
+ self.fail('not_a_list', input_type=type(data).__name__)
+ return [self.child.run_validation(item) for item in data]
+
+ def to_representation(self, data):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ return [self.child.to_representation(item) for item in data]
- from compat import Image
- assert Image is not None, 'PIL must be installed for ImageField support'
- # We need to get a file object for PIL. We might have a path or we might
- # have to read the data into memory.
- if hasattr(data, 'temporary_file_path'):
- file = data.temporary_file_path()
- else:
- if hasattr(data, 'read'):
- file = BytesIO(data.read())
- else:
- file = BytesIO(data['content'])
+class DictField(Field):
+ child = _UnvalidatedField()
+ initial = {}
+ default_error_messages = {
+ 'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".')
+ }
- try:
- # load() could spot a truncated JPEG, but it loads the entire
- # image in memory, which is a DoS vector. See #3848 and #18520.
- # verify() must be called immediately after the constructor.
- Image.open(file).verify()
- except ImportError:
- # Under PyPy, it is possible to import PIL. However, the underlying
- # _imaging C module isn't available, so an ImportError will be
- # raised. Catch and re-raise.
- raise
- except Exception: # Python Imaging Library doesn't recognize it as an image
- raise ValidationError(self.error_messages['invalid_image'])
- if hasattr(f, 'seek') and callable(f.seek):
- f.seek(0)
- return f
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(DictField, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # dictionaries in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
+ """
+ Dicts of native values <- Dicts of primitive datatypes.
+ """
+ if html.is_html_input(data):
+ data = html.parse_html_dict(data)
+ if not isinstance(data, dict):
+ self.fail('not_a_dict', input_type=type(data).__name__)
+ return dict([
+ (six.text_type(key), self.child.run_validation(value))
+ for key, value in data.items()
+ ])
+
+ def to_representation(self, value):
+ """
+ List of object instances -> List of dicts of primitive datatypes.
+ """
+ return dict([
+ (six.text_type(key), self.child.to_representation(val))
+ for key, val in value.items()
+ ])
-class SerializerMethodField(Field):
+# Miscellaneous field types...
+
+class ReadOnlyField(Field):
+ """
+ A read-only field that simply returns the field value.
+
+ If the field is a method with no parameters, the method will be called
+ and it's return value used as the representation.
+
+ For example, the following would call `get_expiry_date()` on the object:
+
+ class ExampleSerializer(self):
+ expiry_date = ReadOnlyField(source='get_expiry_date')
+ """
+
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ super(ReadOnlyField, self).__init__(**kwargs)
+
+ def to_representation(self, value):
+ return value
+
+
+class HiddenField(Field):
+ """
+ A hidden field does not take input from the user, or present any output,
+ but it does populate a field in `validated_data`, based on its default
+ value. This is particularly useful when we have a `unique_for_date`
+ constraint on a pair of fields, as we need some way to include the date in
+ the validated data.
"""
- A field that gets its value by calling a method on the serializer it's attached to.
+ def __init__(self, **kwargs):
+ assert 'default' in kwargs, 'default is a required argument.'
+ kwargs['write_only'] = True
+ super(HiddenField, self).__init__(**kwargs)
+
+ def get_value(self, dictionary):
+ # We always use the default value for `HiddenField`.
+ # User input is never provided or accepted.
+ return empty
+
+ def to_internal_value(self, data):
+ return data
+
+
+class SerializerMethodField(Field):
"""
+ A read-only field that get its representation from calling a method on the
+ parent serializer class. The method called will be of the form
+ "get_{field_name}", and should take a single argument, which is the
+ object being serialized.
- def __init__(self, method_name):
+ For example:
+
+ class ExampleSerializer(self):
+ extra_info = SerializerMethodField()
+
+ def get_extra_info(self, obj):
+ return ... # Calculate some data to return.
+ """
+ def __init__(self, method_name=None, **kwargs):
self.method_name = method_name
- super(SerializerMethodField, self).__init__()
+ kwargs['source'] = '*'
+ kwargs['read_only'] = True
+ super(SerializerMethodField, self).__init__(**kwargs)
+
+ def bind(self, field_name, parent):
+ # In order to enforce a consistent style, we error if a redundant
+ # 'method_name' argument has been used. For example:
+ # my_field = serializer.CharField(source='my_field')
+ default_method_name = 'get_{field_name}'.format(field_name=field_name)
+ assert self.method_name != default_method_name, (
+ "It is redundant to specify `%s` on SerializerMethodField '%s' in "
+ "serializer '%s', because it is the same as the default method name. "
+ "Remove the `method_name` argument." %
+ (self.method_name, field_name, parent.__class__.__name__)
+ )
+
+ # The method name should default to `get_{field_name}`.
+ if self.method_name is None:
+ self.method_name = default_method_name
+
+ super(SerializerMethodField, self).bind(field_name, parent)
+
+ def to_representation(self, value):
+ method = getattr(self.parent, self.method_name)
+ return method(value)
+
+
+class ModelField(Field):
+ """
+ A generic field that can be used against an arbitrary model field.
- def field_to_native(self, obj, field_name):
- value = getattr(self.parent, self.method_name)(obj)
- return self.to_native(value)
+ This is used by `ModelSerializer` when dealing with custom model fields,
+ that do not have a serializer field to be mapped to.
+ """
+ default_error_messages = {
+ 'max_length': _('Ensure this field has no more than {max_length} characters.'),
+ }
+
+ def __init__(self, model_field, **kwargs):
+ self.model_field = model_field
+ # The `max_length` option is supported by Django's base `Field` class,
+ # so we'd better support it here.
+ max_length = kwargs.pop('max_length', None)
+ super(ModelField, self).__init__(**kwargs)
+ if max_length is not None:
+ message = self.error_messages['max_length'].format(max_length=max_length)
+ self.validators.append(MaxLengthValidator(max_length, message=message))
+
+ def to_internal_value(self, data):
+ rel = getattr(self.model_field, 'rel', None)
+ if rel is not None:
+ return rel.to._meta.get_field(rel.field_name).to_python(data)
+ return self.model_field.to_python(data)
+
+ def get_attribute(self, obj):
+ # We pass the object instance onto `to_representation`,
+ # not just the field attribute.
+ return obj
+
+ def to_representation(self, obj):
+ value = self.model_field._get_val_from_obj(obj)
+ if is_protected_type(value):
+ return value
+ return self.model_field.value_to_string(obj)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index c058bc71..9a84efa2 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -3,8 +3,12 @@ Provides generic filtering backends that can be used to filter the results
returned by list views.
"""
from __future__ import unicode_literals
+
+from django.core.exceptions import ImproperlyConfigured
from django.db import models
-from rest_framework.compat import django_filters, six
+from django.utils import six
+from rest_framework.compat import django_filters, guardian, get_model_name
+from rest_framework.settings import api_settings
from functools import reduce
import operator
@@ -42,7 +46,7 @@ class DjangoFilterBackend(BaseFilterBackend):
if filter_class:
filter_model = filter_class.Meta.model
- assert issubclass(filter_model, queryset.model), \
+ assert issubclass(queryset.model, filter_model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
@@ -61,20 +65,21 @@ class DjangoFilterBackend(BaseFilterBackend):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
- return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
+ return filter_class(request.query_params, queryset=queryset).qs
return queryset
class SearchFilter(BaseFilterBackend):
- search_param = 'search' # The URL query parameter used for the search.
+ # The URL query parameter used for the search.
+ search_param = api_settings.SEARCH_PARAM
def get_search_terms(self, request):
"""
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
- params = request.QUERY_PARAMS.get(self.search_param, '')
+ params = request.query_params.get(self.search_param, '')
return params.replace(',', ' ').split()
def construct_search(self, field_name):
@@ -93,28 +98,39 @@ class SearchFilter(BaseFilterBackend):
if not search_fields:
return queryset
- orm_lookups = [self.construct_search(str(search_field))
+ orm_lookups = [self.construct_search(six.text_type(search_field))
for search_field in search_fields]
for search_term in self.get_search_terms(request):
or_queries = [models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups]
- queryset = queryset.filter(reduce(operator.or_, or_queries))
+ queryset = queryset.filter(reduce(operator.or_, or_queries)).distinct()
return queryset
class OrderingFilter(BaseFilterBackend):
- ordering_param = 'ordering' # The URL query parameter used for the ordering.
+ # The URL query parameter used for the ordering.
+ ordering_param = api_settings.ORDERING_PARAM
+ ordering_fields = None
- def get_ordering(self, request):
+ def get_ordering(self, request, queryset, view):
"""
- Search terms are set by a ?search=... query parameter,
- and may be comma and/or whitespace delimited.
+ Ordering is set by a comma delimited ?ordering=... query parameter.
+
+ The `ordering` query parameter can be overridden by setting
+ the `ordering_param` value on the OrderingFilter or by
+ specifying an `ORDERING_PARAM` value in the API settings.
"""
- params = request.QUERY_PARAMS.get(self.ordering_param)
+ params = request.query_params.get(self.ordering_param)
if params:
- return [param.strip() for param in params.split(',')]
+ fields = [param.strip() for param in params.split(',')]
+ ordering = self.remove_invalid_fields(queryset, fields, view)
+ if ordering:
+ return ordering
+
+ # No ordering was included, or all the ordering fields were invalid
+ return self.get_default_ordering(view)
def get_default_ordering(self, view):
ordering = getattr(view, 'ordering', None)
@@ -122,22 +138,53 @@ class OrderingFilter(BaseFilterBackend):
return (ordering,)
return ordering
- def remove_invalid_fields(self, queryset, ordering):
- field_names = [field.name for field in queryset.model._meta.fields]
- return [term for term in ordering if term.lstrip('-') in field_names]
+ def remove_invalid_fields(self, queryset, fields, view):
+ valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
+
+ if valid_fields is None:
+ # Default to allowing filtering on serializer fields
+ serializer_class = getattr(view, 'serializer_class')
+ if serializer_class is None:
+ msg = ("Cannot use %s on a view which does not have either a "
+ "'serializer_class' or 'ordering_fields' attribute.")
+ raise ImproperlyConfigured(msg % self.__class__.__name__)
+ valid_fields = [
+ field.source or field_name
+ for field_name, field in serializer_class().fields.items()
+ if not getattr(field, 'write_only', False)
+ ]
+ elif valid_fields == '__all__':
+ # View explicitly allows filtering on any model field
+ valid_fields = [field.name for field in queryset.model._meta.fields]
+ valid_fields += queryset.query.aggregates.keys()
+
+ return [term for term in fields if term.lstrip('-') in valid_fields]
def filter_queryset(self, request, queryset, view):
- ordering = self.get_ordering(request)
-
- if ordering:
- # Skip any incorrect parameters
- ordering = self.remove_invalid_fields(queryset, ordering)
-
- if not ordering:
- # Use 'ordering' attribtue by default
- ordering = self.get_default_ordering(view)
+ ordering = self.get_ordering(request, queryset, view)
if ordering:
return queryset.order_by(*ordering)
return queryset
+
+
+class DjangoObjectPermissionsFilter(BaseFilterBackend):
+ """
+ A filter backend that limits results to those where the requesting user
+ has read object level permissions.
+ """
+ def __init__(self):
+ assert guardian, 'Using DjangoObjectPermissionsFilter, but django-guardian is not installed'
+
+ perm_format = '%(app_label)s.view_%(model_name)s'
+
+ def filter_queryset(self, request, queryset, view):
+ user = request.user
+ model_cls = queryset.model
+ kwargs = {
+ 'app_label': model_cls._meta.app_label,
+ 'model_name': get_model_name(model_cls)
+ }
+ permission = self.perm_format % kwargs
+ return guardian.shortcuts.get_objects_for_user(user, permission, queryset)
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 99e9782e..61dcb84a 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -2,25 +2,20 @@
Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
-
-from django.core.exceptions import ImproperlyConfigured, PermissionDenied
-from django.core.paginator import Paginator, InvalidPage
+from django.db.models.query import QuerySet
from django.http import Http404
from django.shortcuts import get_object_or_404 as _get_object_or_404
-from django.utils.translation import ugettext as _
-from rest_framework import views, mixins, exceptions
-from rest_framework.request import clone_request
+from rest_framework import views, mixins
from rest_framework.settings import api_settings
-import warnings
-def get_object_or_404(queryset, **filter_kwargs):
+def get_object_or_404(queryset, *filter_args, **filter_kwargs):
"""
- Same as Django's standard shortcut, but make sure to raise 404
+ Same as Django's standard shortcut, but make sure to also raise 404
if the filter_kwargs don't match the required types.
"""
try:
- return _get_object_or_404(queryset, **filter_kwargs)
+ return _get_object_or_404(queryset, *filter_args, **filter_kwargs)
except (TypeError, ValueError):
raise Http404
@@ -29,205 +24,25 @@ class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
-
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
+ # If you are overriding a view method, it is important that you call
+ # `get_queryset()` instead of accessing the `queryset` property directly,
+ # as `queryset` will get evaluated only once, and those results are cached
+ # for all subsequent requests.
queryset = None
serializer_class = None
- # This shortcut may be used instead of setting either or both
- # of the `queryset`/`serializer_class` attributes, although using
- # the explicit style is generally preferred.
- model = None
-
- # If you want to use object lookups other than pk, set this attribute.
+ # If you want to use object lookups other than pk, set 'lookup_field'.
# For more complex lookup requirements override `get_object()`.
lookup_field = 'pk'
-
- # Pagination settings
- paginate_by = api_settings.PAGINATE_BY
- paginate_by_param = api_settings.PAGINATE_BY_PARAM
- pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
- page_kwarg = 'page'
+ lookup_url_kwarg = None
# The filter backend classes to use for queryset filtering
filter_backends = api_settings.DEFAULT_FILTER_BACKENDS
- # The following attributes may be subject to change,
- # and should be considered private API.
- model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
- paginator_class = Paginator
-
- ######################################
- # These are pending deprecation...
-
- pk_url_kwarg = 'pk'
- slug_url_kwarg = 'slug'
- slug_field = 'slug'
- allow_empty = True
- filter_backend = api_settings.FILTER_BACKEND
-
- def get_serializer_context(self):
- """
- Extra context provided to the serializer class.
- """
- return {
- 'request': self.request,
- 'format': self.format_kwarg,
- 'view': self
- }
-
- def get_serializer(self, instance=None, data=None,
- files=None, many=False, partial=False):
- """
- Return the serializer instance that should be used for validating and
- deserializing input, and for serializing output.
- """
- serializer_class = self.get_serializer_class()
- context = self.get_serializer_context()
- return serializer_class(instance, data=data, files=files,
- many=many, partial=partial, context=context)
-
- def get_pagination_serializer(self, page):
- """
- Return a serializer instance to use with paginated data.
- """
- class SerializerClass(self.pagination_serializer_class):
- class Meta:
- object_serializer_class = self.get_serializer_class()
-
- pagination_serializer_class = SerializerClass
- context = self.get_serializer_context()
- return pagination_serializer_class(instance=page, context=context)
-
- def paginate_queryset(self, queryset, page_size=None):
- """
- Paginate a queryset if required, either returning a page object,
- or `None` if pagination is not configured for this view.
- """
- deprecated_style = False
- if page_size is not None:
- warnings.warn('The `page_size` parameter to `paginate_queryset()` '
- 'is due to be deprecated. '
- 'Note that the return style of this method is also '
- 'changed, and will simply return a page object '
- 'when called without a `page_size` argument.',
- PendingDeprecationWarning, stacklevel=2)
- deprecated_style = True
- else:
- # Determine the required page size.
- # If pagination is not configured, simply return None.
- page_size = self.get_paginate_by()
- if not page_size:
- return None
-
- if not self.allow_empty:
- warnings.warn(
- 'The `allow_empty` parameter is due to be deprecated. '
- 'To use `allow_empty=False` style behavior, You should override '
- '`get_queryset()` and explicitly raise a 404 on empty querysets.',
- PendingDeprecationWarning, stacklevel=2
- )
-
- paginator = self.paginator_class(queryset, page_size,
- allow_empty_first_page=self.allow_empty)
- page_kwarg = self.kwargs.get(self.page_kwarg)
- page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg)
- page = page_kwarg or page_query_param or 1
- try:
- page_number = int(page)
- except ValueError:
- if page == 'last':
- page_number = paginator.num_pages
- else:
- raise Http404(_("Page is not 'last', nor can it be converted to an int."))
- try:
- page = paginator.page(page_number)
- except InvalidPage as e:
- raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
- 'page_number': page_number,
- 'message': str(e)
- })
-
- if deprecated_style:
- return (paginator, page, page.object_list, page.has_other_pages())
- return page
-
- def filter_queryset(self, queryset):
- """
- Given a queryset, filter it with whichever filter backend is in use.
-
- You are unlikely to want to override this method, although you may need
- to call it either from a list view, or from a custom `get_object`
- method if you want to apply the configured filtering backend to the
- default queryset.
- """
- filter_backends = self.filter_backends or []
- if not filter_backends and self.filter_backend:
- warnings.warn(
- 'The `filter_backend` attribute and `FILTER_BACKEND` setting '
- 'are due to be deprecated in favor of a `filter_backends` '
- 'attribute and `DEFAULT_FILTER_BACKENDS` setting, that take '
- 'a *list* of filter backend classes.',
- PendingDeprecationWarning, stacklevel=2
- )
- filter_backends = [self.filter_backend]
-
- for backend in filter_backends:
- queryset = backend().filter_queryset(self.request, queryset, self)
- return queryset
-
- ########################
- ### The following methods provide default implementations
- ### that you may want to override for more complex cases.
-
- def get_paginate_by(self, queryset=None):
- """
- Return the size of pages to use with pagination.
-
- If `PAGINATE_BY_PARAM` is set it will attempt to get the page size
- from a named query parameter in the url, eg. ?page_size=100
-
- Otherwise defaults to using `self.paginate_by`.
- """
- if queryset is not None:
- warnings.warn('The `queryset` parameter to `get_paginate_by()` '
- 'is due to be deprecated.',
- PendingDeprecationWarning, stacklevel=2)
-
- if self.paginate_by_param:
- query_params = self.request.QUERY_PARAMS
- try:
- return int(query_params[self.paginate_by_param])
- except (KeyError, ValueError):
- pass
-
- return self.paginate_by
-
- def get_serializer_class(self):
- """
- Return the class to use for the serializer.
- Defaults to using `self.serializer_class`.
-
- You may want to override this if you need to provide different
- serializations depending on the incoming request.
-
- (Eg. admins get full serialization, others get basic serialization)
- """
- serializer_class = self.serializer_class
- if serializer_class is not None:
- return serializer_class
-
- assert self.model is not None, \
- "'%s' should either include a 'serializer_class' attribute, " \
- "or use the 'model' attribute as a shortcut for " \
- "automatically generating a serializer class." \
- % self.__class__.__name__
-
- class DefaultSerializer(self.model_serializer_class):
- class Meta:
- model = self.model
- return DefaultSerializer
+ # The style to use for queryset pagination.
+ pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get_queryset(self):
"""
@@ -235,21 +50,28 @@ class GenericAPIView(views.APIView):
This must be an iterable, and may be a queryset.
Defaults to using `self.queryset`.
+ This method should always be used rather than accessing `self.queryset`
+ directly, as `self.queryset` gets evaluated only once, and those results
+ are cached for all subsequent requests.
+
You may want to override this if you need to provide different
querysets depending on the incoming request.
(Eg. return a list of items that is specific to the user)
"""
- if self.queryset is not None:
- return self.queryset._clone()
-
- if self.model is not None:
- return self.model._default_manager.all()
+ assert self.queryset is not None, (
+ "'%s' should either include a `queryset` attribute, "
+ "or override the `get_queryset()` method."
+ % self.__class__.__name__
+ )
- raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'"
- % self.__class__.__name__)
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated on each request.
+ queryset = queryset.all()
+ return queryset
- def get_object(self, queryset=None):
+ def get_object(self):
"""
Returns the object the view is displaying.
@@ -257,41 +79,19 @@ class GenericAPIView(views.APIView):
queryset lookups. Eg if objects are referenced using multiple
keyword arguments in the url conf.
"""
- # Determine the base queryset to use.
- if queryset is None:
- queryset = self.filter_queryset(self.get_queryset())
- else:
- pass # Deprecation warning
+ queryset = self.filter_queryset(self.get_queryset())
# Perform the lookup filtering.
- pk = self.kwargs.get(self.pk_url_kwarg, None)
- slug = self.kwargs.get(self.slug_url_kwarg, None)
- lookup = self.kwargs.get(self.lookup_field, None)
-
- if lookup is not None:
- filter_kwargs = {self.lookup_field: lookup}
- elif pk is not None and self.lookup_field == 'pk':
- warnings.warn(
- 'The `pk_url_kwarg` attribute is due to be deprecated. '
- 'Use the `lookup_field` attribute instead',
- PendingDeprecationWarning
- )
- filter_kwargs = {'pk': pk}
- elif slug is not None and self.lookup_field == 'pk':
- warnings.warn(
- 'The `slug_url_kwarg` attribute is due to be deprecated. '
- 'Use the `lookup_field` attribute instead',
- PendingDeprecationWarning
- )
- filter_kwargs = {self.slug_field: slug}
- else:
- raise ImproperlyConfigured(
- 'Expected view %s to be called with a URL keyword argument '
- 'named "%s". Fix your URL conf, or set the `.lookup_field` '
- 'attribute on the view correctly.' %
- (self.__class__.__name__, self.lookup_field)
- )
+ lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
+ assert lookup_url_kwarg in self.kwargs, (
+ 'Expected view %s to be called with a URL keyword argument '
+ 'named "%s". Fix your URL conf, or set the `.lookup_field` '
+ 'attribute on the view correctly.' %
+ (self.__class__.__name__, lookup_url_kwarg)
+ )
+
+ filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
obj = get_object_or_404(queryset, **filter_kwargs)
# May raise a permission denied
@@ -299,68 +99,86 @@ class GenericAPIView(views.APIView):
return obj
- ########################
- ### The following are placeholder methods,
- ### and are intended to be overridden.
- ###
- ### The are not called by GenericAPIView directly,
- ### but are used by the mixin methods.
+ def get_serializer(self, *args, **kwargs):
+ """
+ Return the serializer instance that should be used for validating and
+ deserializing input, and for serializing output.
+ """
+ serializer_class = self.get_serializer_class()
+ kwargs['context'] = self.get_serializer_context()
+ return serializer_class(*args, **kwargs)
- def pre_save(self, obj):
+ def get_serializer_class(self):
"""
- Placeholder method for calling before saving an object.
+ Return the class to use for the serializer.
+ Defaults to using `self.serializer_class`.
+
+ You may want to override this if you need to provide different
+ serializations depending on the incoming request.
- May be used to set attributes on the object that are implicit
- in either the request, or the url.
+ (Eg. admins get full serialization, others get basic serialization)
"""
- pass
+ assert self.serializer_class is not None, (
+ "'%s' should either include a `serializer_class` attribute, "
+ "or override the `get_serializer_class()` method."
+ % self.__class__.__name__
+ )
+
+ return self.serializer_class
- def post_save(self, obj, created=False):
+ def get_serializer_context(self):
"""
- Placeholder method for calling after saving an object.
+ Extra context provided to the serializer class.
+ """
+ return {
+ 'request': self.request,
+ 'format': self.format_kwarg,
+ 'view': self
+ }
+
+ def filter_queryset(self, queryset):
"""
- pass
+ Given a queryset, filter it with whichever filter backend is in use.
- def metadata(self, request):
+ You are unlikely to want to override this method, although you may need
+ to call it either from a list view, or from a custom `get_object`
+ method if you want to apply the configured filtering backend to the
+ default queryset.
"""
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
+ for backend in list(self.filter_backends):
+ queryset = backend().filter_queryset(self.request, queryset, self)
+ return queryset
- We override the default behavior, and add some extra information
- about the required request body for POST and PUT operations.
+ @property
+ def paginator(self):
"""
- ret = super(GenericAPIView, self).metadata(request)
-
- actions = {}
- for method in ('PUT', 'POST'):
- if method not in self.allowed_methods:
- continue
-
- cloned_request = clone_request(request, method)
- try:
- # Test global permissions
- self.check_permissions(cloned_request)
- # Test object permissions
- if method == 'PUT':
- self.get_object()
- except (exceptions.APIException, PermissionDenied, Http404):
- pass
+ The paginator instance associated with the view, or `None`.
+ """
+ if not hasattr(self, '_paginator'):
+ if self.pagination_class is None:
+ self._paginator = None
else:
- # If user has appropriate permissions for the view, include
- # appropriate metadata about the fields that should be supplied.
- serializer = self.get_serializer()
- actions[method] = serializer.metadata()
+ self._paginator = self.pagination_class()
+ return self._paginator
- if actions:
- ret['actions'] = actions
+ def paginate_queryset(self, queryset):
+ """
+ Return a single page of results, or `None` if pagination is disabled.
+ """
+ if self.paginator is None:
+ return None
+ return self.paginator.paginate_queryset(queryset, self.request, view=self)
- return ret
+ def get_paginated_response(self, data):
+ """
+ Return a paginated style `Response` object for the given output data.
+ """
+ assert self.paginator is not None
+ return self.paginator.get_paginated_response(data)
-##########################################################
-### Concrete view classes that provide method handlers ###
-### by composing the mixin classes with the base view. ###
-##########################################################
+# Concrete view classes that provide method handlers
+# by composing the mixin classes with the base view.
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
@@ -473,27 +291,3 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
-
-
-##########################
-### Deprecated classes ###
-##########################
-
-class MultipleObjectAPIView(GenericAPIView):
- def __init__(self, *args, **kwargs):
- warnings.warn(
- 'Subclassing `MultipleObjectAPIView` is due to be deprecated. '
- 'You should simply subclass `GenericAPIView` instead.',
- PendingDeprecationWarning, stacklevel=2
- )
- super(MultipleObjectAPIView, self).__init__(*args, **kwargs)
-
-
-class SingleObjectAPIView(GenericAPIView):
- def __init__(self, *args, **kwargs):
- warnings.warn(
- 'Subclassing `SingleObjectAPIView` is due to be deprecated. '
- 'You should simply subclass `GenericAPIView` instead.',
- PendingDeprecationWarning, stacklevel=2
- )
- super(SingleObjectAPIView, self).__init__(*args, **kwargs)
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.mo b/rest_framework/locale/ar/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..fe1b676c
--- /dev/null
+++ b/rest_framework/locale/ar/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.po b/rest_framework/locale/ar/LC_MESSAGES/django.po
new file mode 100644
index 00000000..a910a7c9
--- /dev/null
+++ b/rest_framework/locale/ar/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Eyad Toma <d.eyad.t@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Arabic (http://www.transifex.com/projects/p/django-rest-framework/language/ar/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ar\n"
+"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "اسم المستخدم/كلمة السر غير صحيحين."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "المستخدم غير مفعل او تم حذفه."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "حدث خطأ في المخدم."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "بيانات الدخول غير صحيحة."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "لم يتم تزويد بيانات الدخول."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "ليس لديك صلاحية للقيام بهذا الإجراء."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "غير موجود."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "هذا الحقل مطلوب."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "لا يمكن لهذا الحقل ان يكون فارغاً null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" ليس قيمة منطقية."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "لا يمكن لهذا الحقل ان يكون فارغاً."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "تأكد ان الحقل لا يزيد عن {max_length} محرف."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "تأكد ان الحقل {min_length} محرف على الاقل."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "عليك ان تدخل بريد إلكتروني صالح."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "هذه القيمة لا تطابق النمط المطلوب."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "الرجاء إدخال رابط إلكتروني صالح."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "الرجاء إدخال رقم صحيح صالح."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "تأكد ان القيمة أقل أو تساوي {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "تأكد ان القيمة أكبر أو تساوي {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "الرجاء إدخال رقم صالح."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "تأكد ان القيمة لا تحوي أكثر من {max_digits} رقم."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة التاريخ غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "صيغة الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" ليست واحدة من الخيارات الصالحة."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "لم يتم إرسال أي ملف."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "الملف الذي تم إرساله فارغ."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "تأكد ان اسم الملف لا يحوي أكثر من {max_length} محرف (الإسم المرسل يحوي {length} محرف)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "رقم الصفحة \"{page_number}\" غير صالح : {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "معرف العنصر \"{pk_value}\" غير صالح - العنصر غير موجود."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "قيمة غير صالحة."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "حساب المستخدم غير مفعل."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "تعذر تسجيل الدخول بالبيانات التي ادخلتها."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "يجب أن تتضمن \"اسم المستخدم\" و \"كلمة المرور\"."
diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.mo b/rest_framework/locale/cs/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..a5e67713
--- /dev/null
+++ b/rest_framework/locale/cs/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.po b/rest_framework/locale/cs/LC_MESSAGES/django.po
new file mode 100644
index 00000000..50e7034b
--- /dev/null
+++ b/rest_framework/locale/cs/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Jirka Vejrazka <Jirka.Vejrazka@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Czech (http://www.transifex.com/projects/p/django-rest-framework/language/cs/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: cs\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Chybná hlavička. Nebyly poskytnuty přihlašovací údaje."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Chybná hlavička. Přihlašovací údaje by neměly obsahovat mezery."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Chybná hlavička. Přihlašovací údaje nebyly správně zakódovány pomocí base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Chybné uživatelské jméno nebo heslo."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Chybná hlavička tokenu. Nebyly zadány přihlašovací údaje."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Chybná hlavička tokenu. Přihlašovací údaje by neměly obsahovat mezery."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Chybný token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Uživatelský účet je neaktivní nebo byl smazán."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Chyba na straně serveru."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Neplatný formát požadavku."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Chybné přihlašovací údaje."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Přihlašovací údaje nebyly zadány."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "K této akci nemáte oprávnění."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nenalezeno."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoda \"{method}\" není povolena."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nelze vyhovět požadavku v hlavičce Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nepodporovaný media type \"{media_type}\" v požadavku."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Pořadavek byl limitován kvůli omezení počtu požadavků za časovou periodu."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Toto pole je vyžadováno."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Toto pole nesmí být prázdné (null)."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" nelze použít jako typ boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Toto pole nesmí být prázdné.."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Zkontrolujte, že toto pole není delší než {max_length} znaků."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Zkontrolujte, že toto obsahuje alespoň {min_length} znaků"
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Vložte platnou e-mailovou adresu."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Hodnota v tomto poli neodpovídá požadovanému formátu."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Vložte platnou \"zkrácenou formu\" obsahující pouze malá písmena, čísla, spojovník nebo podtržítko."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Vložte platný odkaz."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Je vyžadováno číslo."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Zkontrolujte, že hodnota je menší nebo rovna {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Zkontrolujte, že hodnota je větší nebo rovna {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Řetězec je příliš dlouhý"
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Je vyžadováno číslo."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Zkontrolujte, že číslo neobsahuje více než {max_digits} čislic."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Zkontrolujte, že číslo nemá více než {max_decimal_places} desetinných míst."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Zkontrolujte, že číslo neobsahuje více než {max_whole_digits} čislic před desetinnou čárkou."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát data a času. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Bylo zadáno pouze datum místo data a času."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát data. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Bylo zadáno datum a čas, místo samotného data."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Chybný formát času. Použijte jeden z těchto formátů: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" není platnou možností."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Byl očekáván seznam položek ale nalezen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Nebyl zaslán žádný soubor."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Zaslaná data neobsahují soubor. Zkontrolujte typ kódování ve formuláři."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nebylo možno zjistit jméno souboru."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Zaslaný soubor je prázdný."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Zajistěte, aby jméno souboru obsahovalo maximálně {max_length} znaků (teď má {length} znaků)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Nahrajte platný obrázek. Nahraný soubor buď není obrázkem, nebo je poškozen."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Chybné čislo stránky \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Chybný primární klíč \"{pk_value}\" - objekt neexistuje."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Chybný typ. Byl přijat typ {data_type} místo hodnoty primárního klíče."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Chybný odkaz - nebyla nalezena žádní shoda."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Chybný odkaz - byla nalezena neplatná shoda."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Chybný odkaz - objekt neexistuje."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Chybný typ. Byl přijat typ {data_type} místo očekávaného odkazu."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objekt s {slug_name}={value} neexistuje."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Chybná hodnota."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Chybná data. Byl přijat typ {datatype} místo očakávaného slovníku."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Tato položka musí být unikátní."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Položka {field_names} musí tvořit unikátní množinu."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Tato položka musí být pro datum \"{date_field}\" unikátní."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Tato položka musí být pro měsíc \"{date_field}\" unikátní."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Tato položka musí být pro rok \"{date_field}\" unikátní."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Chybné číslo verze v hlavičce Accept"
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Chybné číslo verze v odkazu."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Chybné číslo verze v hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Chybné čislo verze v URL parametru."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Uživatelský účet je zamčen."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Se zadanými údaji nebylo možné se přihlásit."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musí obsahovat \"uživatelské jméno! a \"heslo\"."
diff --git a/rest_framework/locale/da/LC_MESSAGES/django.mo b/rest_framework/locale/da/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..f947f907
--- /dev/null
+++ b/rest_framework/locale/da/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/da/LC_MESSAGES/django.po b/rest_framework/locale/da/LC_MESSAGES/django.po
new file mode 100644
index 00000000..e00ffadf
--- /dev/null
+++ b/rest_framework/locale/da/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mikkel Munch Mortensen <3xm@detfalskested.dk>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Danish (http://www.transifex.com/projects/p/django-rest-framework/language/da/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: da\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ugyldig basic header. Ingen legitimation angivet."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ugyldig basic header. Legitimationsstrenge må ikke indeholde mellemrum."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ugyldig basic header. Legitimationen er ikke base64 encoded på korrekt vis."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ugyldigt brugernavn/kodeord."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ugyldig token header."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ugyldig token header. Token-strenge må ikke indeholde mellemrum."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ugyldigt token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Inaktiv eller slettet bruger."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Der er sket en serverfejl."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Misdannet forespørgsel."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Ugyldig legitimation til autentificering."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Legitimation til autentificering blev ikke angivet."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Du har ikke lov til at udføre denne handling."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Ikke fundet."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoden \"{method}\" er ikke tilladt."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kunne ikke efterkomme forespørgslens Accept header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Forespørgslens media type, \"{media_type}\", er ikke understøttet."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Forespørgslen blev neddroslet."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Dette felt er påkrævet."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Dette felt må ikke være null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" er ikke en tilladt boolsk værdi."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Dette felt må ikke være tomt."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Tjek at dette felt ikke indeholder flere end {max_length} tegn."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Tjek at dette felt indeholder mindst {min_length} tegn."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Angiv en gyldig e-mailadresse."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Denne værdi passer ikke med det påkrævede mønster."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Indtast en gyldig \"slug\", bestående af bogstaver, tal, bund- og bindestreger."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Indtast en gyldig URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Et gyldigt heltal er påkrævet."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Tjek at værdien er mindre end eller lig med {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Tjek at værdien er større end eller lig med {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Strengværdien er for stor."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Et gyldigt tal er påkrævet."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Tjek at der ikke er flere end {max_digits} cifre i alt."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Tjek at der ikke er flere end {max_decimal_places} cifre efter kommaet."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Tjek at der ikke er flere end {max_whole_digits} cifre før kommaet."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datotid har et forkert format. Brug i stedet et af disse formater: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Forventede en datotid, men fik en dato."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Dato har et forkert format. Brug i stedet et af disse formater: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Forventede en dato men fik en datotid."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Klokkeslæt har forkert format. Brug i stedet et af disse formater: {format}. "
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" er ikke et gyldigt valg."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Forventede en liste, men fik noget af typen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ingen medsendt fil."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Det medsendte data var ikke en fil. Tjek typen af indkodning på formularen."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Filnavnet kunne ikke afgøres."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Den medsendte fil er tom."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Sørg for at filnavnet er højst {max_length} langt (det er {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Medsend et gyldigt billede. Den medsendte fil var enten ikke et billede eller billedfilen var ødelagt."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Ugyldig side \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Ugyldig primærnøgle \"{pk_value}\" - objektet findes ikke."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Ugyldig type. Forventet værdi er primærnøgle, fik {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Ugyldigt hyperlink - intet URL match."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Ugyldigt hyperlink - forkert URL match."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Ugyldigt hyperlink - objektet findes ikke."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Forkert type. Forventede en URL-streng, fik {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Object med {slug_name}={value} findes ikke."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ugyldig værdi."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ugyldig data. Forventede en dictionary, men fik {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Dette felt skal være unikt."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Felterne {field_names} skal udgøre et unikt sæt."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-datoen."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-måneden."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Dette felt skal være unikt for \"{date_field}\"-året."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Ugyldig version i \"Accept\" headeren."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Ugyldig version i URL-stien."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Ugyldig version i hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Ugyldig version i forespørgselsparameteren."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Brugerkontoen er deaktiveret."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kunne ikke logge ind med den angivne legitimation."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Skal indeholde \"username\" og \"password\"."
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.mo b/rest_framework/locale/de/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..48245c60
--- /dev/null
+++ b/rest_framework/locale/de/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/de/LC_MESSAGES/django.po b/rest_framework/locale/de/LC_MESSAGES/django.po
new file mode 100644
index 00000000..74bee416
--- /dev/null
+++ b/rest_framework/locale/de/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Thomas Tanner, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: German (http://www.transifex.com/projects/p/django-rest-framework/language/de/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: de\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ungültiger basic header. Keine Zugangsdaten angegeben."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ungültiger basic header. Zugangsdaten sollen keine Leerzeichen enthalten."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ungültiger basic header. Zugangsdaten sind nicht korrekt mit base64 kodiert."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ungültiger Benutzername/Passwort"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ungültiger token header. Keine Zugangsdaten angegeben."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ungültiger token header. Zugangsdaten sollen keine Leerzeichen enthalten."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ungültiges Token"
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Benutzer inaktiv oder gelöscht."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ein Serverfehler ist aufgetreten."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Fehlerhafte Anfrage."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Falsche Anmeldedaten."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Anmeldedaten fehlen."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Sie sind nicht berechtigt, diese Aktion durchzuführen."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nicht gefunden."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Methode \"{method}\" nicht erlaubt."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kann den Accept header der Anfrage nicht erfüllen."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nicht unterstützter Medientyp \"{media_type}\" in der Anfrage."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Die Anfrage wurde gedrosselt."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Dieses Feld ist erforderlich."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Dieses Feld darf nicht Null sein."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" ist kein gültiger Boole'scher Wert."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Dieses Feld darf nicht leer sein."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Stelle sicher, dass dieses Feld nicht mehr als {max_length} Zeichen lang ist."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Stelle sicher, dass dieses Feld mindestens {min_length} Zeichen lang ist."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Gebe eine gültige E-Mail Adresse an."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Dieser Wert passt nicht zu dem erforderlichen Muster."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Gebe ein gültiges \"slug\" aus Buchstaben, Ziffern, Unterstrichen und Minuszeichen ein."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Gebe eine gültige URL ein."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Eine gültige Ganzzahl ist erforderlich."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Stelle sicher, dass dieser Wert kleiner oder gleich {max_value} ist."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Stelle sicher, dass dieser Wert größer oder gleich {max_value} ist."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Zeichenkette zu lang."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Eine gültige Zahl ist erforderlich."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Stelle sicher, dass es insgesamt nicht mehr als {max_digits} Ziffern lang ist."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Stelle sicher, dass es nicht mehr als {max_decimal_places} Nachkommastellen lang ist."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Stelle sicher, dass es nicht mehr als {max_whole_places} Stellen vor dem Komma lang ist."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datum- und Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Erwarte eine Datum- und Zeitangabe, erhielt aber ein Datum."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Datum hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Erwarte ein Datum, erhielt aber eine Datum- und Zeitangabe."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" ist keine gültige Option."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Erwarte eine Liste von Elementen, erhielt aber den Typ \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Es wurde keine Datei übermittelt."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Die übermittelten Daten sind keine Datei. Prüfe den Kodierungstyp im Formular."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Der Dateiname konnte nicht ermittelt werden."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Die übermittelte Datei ist leer."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ungültiger Wert."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ungültige Daten. Dictionary erwartet, aber {datatype} erhalten."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Dieses Feld muss eineindeutig sein."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Benutzerkonto ist gesperrt."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kann nicht mit den angegeben Zugangsdaten anmelden."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"username\" und \"password\" sind erforderlich."
diff --git a/rest_framework/locale/en/LC_MESSAGES/django.mo b/rest_framework/locale/en/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..746915ff
--- /dev/null
+++ b/rest_framework/locale/en/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/en/LC_MESSAGES/django.po b/rest_framework/locale/en/LC_MESSAGES/django.po
new file mode 100644
index 00000000..f3db69e5
--- /dev/null
+++ b/rest_framework/locale/en/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: English (http://www.transifex.com/projects/p/django-rest-framework/language/en/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: en\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Invalid basic header. No credentials provided."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Invalid basic header. Credentials string should not contain spaces."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Invalid basic header. Credentials not correctly base64 encoded."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Invalid username/password."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Invalid token header. No credentials provided."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Invalid token header. Token string should not contain spaces."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Invalid token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "User inactive or deleted."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "A server error occurred."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Malformed request."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Incorrect authentication credentials."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Authentication credentials were not provided."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "You do not have permission to perform this action."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Not found."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Method \"{method}\" not allowed."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Could not satisfy the request Accept header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Unsupported media type \"{media_type}\" in request."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Request was throttled."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "This field is required."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "This field may not be null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" is not a valid boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "This field may not be blank."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Ensure this field has no more than {max_length} characters."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Ensure this field has at least {min_length} characters."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Enter a valid email address."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "This value does not match the required pattern."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Enter a valid \"slug\" consisting of letters, numbers, underscores or hyphens."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Enter a valid URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr "\"{value}\" is not a valid UUID."
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "A valid integer is required."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Ensure this value is less than or equal to {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Ensure this value is greater than or equal to {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "String value too large."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "A valid number is required."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Ensure that there are no more than {max_digits} digits in total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Ensure that there are no more than {max_decimal_places} decimal places."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Ensure that there are no more than {max_whole_digits} digits before the decimal point."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Expected a datetime but got a date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Date has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Expected a date but got a datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time has wrong format. Use one of these formats instead: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" is not a valid choice."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Expected a list of items but got type \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "No file was submitted."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "The submitted data was not a file. Check the encoding type on the form."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "No filename could be determined."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "The submitted file is empty."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Ensure this filename has at most {max_length} characters (it has {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Upload a valid image. The file you uploaded was either not an image or a corrupted image."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr "Expected a dictionary of items but got type \"{input_type}\"."
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Invalid page \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr "Invalid cursor"
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Invalid pk \"{pk_value}\" - object does not exist."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Incorrect type. Expected pk value, received {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Invalid hyperlink - No URL match."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Invalid hyperlink - Incorrect URL match."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Invalid hyperlink - Object does not exist."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Incorrect type. Expected URL string, received {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Object with {slug_name}={value} does not exist."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Invalid value."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Invalid data. Expected a dictionary, but got {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "This field must be unique."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "The fields {field_names} must make a unique set."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "This field must be unique for the \"{date_field}\" date."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "This field must be unique for the \"{date_field}\" month."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "This field must be unique for the \"{date_field}\" year."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Invalid version in \"Accept\" header."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Invalid version in URL path."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Invalid version in hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Invalid version in query parameter."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "User account is disabled."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Unable to log in with provided credentials."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Must include \"username\" and \"password\"."
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.mo b/rest_framework/locale/en_US/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..eb60d9d7
--- /dev/null
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.po b/rest_framework/locale/en_US/LC_MESSAGES/django.po
new file mode 100644
index 00000000..11d94e9c
--- /dev/null
+++ b/rest_framework/locale/en_US/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:40+0000\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\n"
+"Language: \n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93 views.py:77
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid "Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid "The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: views.py:81
+msgid "Permission denied."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/es/LC_MESSAGES/django.mo b/rest_framework/locale/es/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..814db7be
--- /dev/null
+++ b/rest_framework/locale/es/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/es/LC_MESSAGES/django.po b/rest_framework/locale/es/LC_MESSAGES/django.po
new file mode 100644
index 00000000..28ef893d
--- /dev/null
+++ b/rest_framework/locale/es/LC_MESSAGES/django.po
@@ -0,0 +1,327 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# José Padilla <jpadilla@webapplicate.com>, 2015
+# Miguel González <migonzalvar@activitycentral.com>, 2015
+# Sergio Infante <rsinfante@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Spanish (http://www.transifex.com/projects/p/django-rest-framework/language/es/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: es\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Cabecera básica inválida. Las credenciales no fueron suministradas."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Cabecera básica inválida. La cadena con las credenciales no debe contener espacios."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Cabecera básica inválida. Las credenciales incorrectamente codificadas en base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nombre de usuario/contraseña inválidos."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Cabecera token inválida. Las credenciales no fueron suministradas."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Cabecera token inválida. La cadena token no debe contener espacios."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token inválido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Usuario inactivo o borrado."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Se ha producido un error en el servidor."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Solicitud con formato incorrecto."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenciales de autenticación incorrectas."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Las credenciales de autenticación no se proveyeron."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Usted no tiene permiso para realizar esta acción."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "No encontrado."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Método \"{method}\" no permitido."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "No se ha podido satisfacer la solicitud de cabecera de Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Tipo de medio \"{media_type}\" incompatible en la solicitud."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Solicitud fue regulada (throttled)."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Este campo es requerido."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Este campo no puede ser nulo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" no es un booleano válido."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Este campo no puede estar en blanco."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Asegúrese de que este campo no tenga más de {max_length} caracteres."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Asegúrese de que este campo tenga al menos {min_length} caracteres."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Introduzca una dirección de correo electrónico válida."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Este valor no coincide con el patrón requerido."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Introduzca un \"slug\" válido consistente en letras, números, guiones o guiones bajos."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Introduzca una URL válida."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Introduzca un número entero válido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Asegúrese de que este valor es menor o igual a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Asegúrese de que este valor es mayor o igual a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Cadena demasiado larga."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Se requiere un número válido."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Asegúrese de que no haya más de {max_digits} dígitos en total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Asegúrese de que no haya más de {max_decimal_places} decimales."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Asegúrese de que no haya más de {max_whole_digits} dígitos en la parte entera."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Fecha/hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Se esperaba un fecha/hora en vez de una fecha."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Fecha con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Se esperaba una fecha en vez de una fecha/hora."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" no es una elección válida."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Se esperaba una lista de elementos en vez del tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "No se envió ningún archivo."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "La información enviada no era un archivo. Compruebe el tipo de codificación del formulario."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "No se pudo determinar un nombre de archivo."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "El archivo enviado está vació."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Asegúrese de que el nombre de archivo no tenga más de {max_length} caracteres (tiene {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Adjunte una imagen válida. El archivo adjunto o bien no es una imagen o bien está dañado."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Página \"{page_number}\" inválida: {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Clave primaria \"{pk_value}\" inválida - objeto no existe."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Tipo incorrecto. Se esperaba valor de clave primaria y se recibió {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Hiperenlace inválido - No hay URL coincidentes."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Hiperenlace inválido - Coincidencia incorrecta de la URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Hiperenlace inválido - Objeto no existe."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Tipo incorrecto. Se esperaba una URL y se recibió {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objeto con {slug_name}={value} no existe."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valor inválido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Datos inválidos. Se esperaba un diccionario pero es un {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Este campo debe ser único."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Los campos {field_names} deben formar un conjunto único."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Este campo debe ser único para el día \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Este campo debe ser único para el mes \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Este campo debe ser único para el año \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Versión inválida en la cabecera \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Versión inválida en la ruta de la URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Versión inválida en el nombre de host."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Versión inválida en el parámetro de consulta."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Cuenta de usuario está deshabilitada."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "No puede iniciar sesión con las credenciales proporcionadas."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Debe incluir \"username\" y \"password\"."
diff --git a/rest_framework/locale/et/LC_MESSAGES/django.mo b/rest_framework/locale/et/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..ca9b6ec4
--- /dev/null
+++ b/rest_framework/locale/et/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/et/LC_MESSAGES/django.po b/rest_framework/locale/et/LC_MESSAGES/django.po
new file mode 100644
index 00000000..dec03d4d
--- /dev/null
+++ b/rest_framework/locale/et/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Tõnis Kärdi <tonis.kardi@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Estonian (http://www.transifex.com/projects/p/django-rest-framework/language/et/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: et\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Vale kasutajatunnus/salasõna."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Kasutaja on inaktiivne või kustutatud."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Väli on kohustuslik."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Väli ei tohi olla tühi."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Sisesta kehtiv e-posti aadress."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Väärtus ei ühti etteantud mustriga."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Sisesta korrektne URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Veendu, et väärtus on väiksem kui või võrdne väärtusega {max_value}. "
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Veendu, et väärtus on suurem kui või võrdne väärtusega {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Sõne on liiga pikk."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Veendu, et kokku pole rohkem kui {max_digits}."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Veendu, et komakohti pole rohkem kui {max_decimal_places}. "
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Kasutajakonto on suletud"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Sisselogimine antud tunnusega ebaõnnestus."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Peab sisaldama \"kasutajatunnust\" ja \"slasõna\"."
diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.mo b/rest_framework/locale/fr/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..68519d45
--- /dev/null
+++ b/rest_framework/locale/fr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.po b/rest_framework/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..e8597c30
--- /dev/null
+++ b/rest_framework/locale/fr/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Etienne Desgagné <etienne.desgagne@evimbec.ca>, 2015
+# Martin Maillard <martin.maillard@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: French (http://www.transifex.com/projects/p/django-rest-framework/language/fr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: fr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "En-tête « basic » non valide. Informations d'identification non fournies."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "En-tête « basic » non valide. Les informations d'identification ne doivent pas contenir d'espaces."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "En-tête « basic » non valide. Encodage base64 des informations d'identification incorrect."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nom d'utilisateur et/ou mot de passe non valide(s)."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "En-tête « token » non valide. Informations d'identification non fournies."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "En-tête « token » non valide. Un token ne doit pas contenir d'espaces."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token non valide."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Utilisateur inactif ou supprimé."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Une erreur du serveur est survenue."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Requête malformée"
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Informations d'authentification incorrectes."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Informations d'authentification non fournies."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Vous n'avez pas la permission d'effectuer cette action."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Méthode \"{method}\" non autorisée."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ce champ est obligatoire."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ce champ ne peut être null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" n'est pas un booléen valide."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ce champ ne peut être vide."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Assurez-vous que ce champ comporte au plus {max_length} caractères."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Assurez-vous que ce champ comporte au moins {min_length} caractères."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Saisissez une adresse email valable."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Cette valeur ne satisfait pas le motif imposé."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Ce champ ne doit contenir que des lettres, des nombres, des tirets bas _ et des traits d'union."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Saisissez une URL valide."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Saisissez un nombre entier valide."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Assurez-vous que cette valeur est inférieure ou égale à {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Assurez-vous que cette valeur est supérieure ou égale à {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Chaîne de caractères trop longue."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Un nombre valide est requis."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_digits} chiffres au total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_decimal_places} chiffres après la virgule."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Assurez-vous qu'il n'y a pas plus de {max_whole_digits} chiffres avant la virgule."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" n'est pas un choix valide."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Aucun fichier n'a été soumis."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "La donnée soumise n'est pas un fichier. Vérifiez le type d'encodage du formulaire."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Le nom de fichier n'a pu être déterminé."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Le fichier soumis est vide."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Assurez-vous que le nom de fichier comporte au plus {max_length} caractères (il en comporte {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Page \"{page_number}\" non valide : {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Clé primaire \"{pk_value}\" non valide - l'objet n'existe pas."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "L'object avec {slug_name}={value} n'existe pas."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valeur non valide."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ce champ doit être unique."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Les champs {field_names} doivent former un ensemble unique."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Ce champ doit être unique pour la date \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Ce champ doit être unique pour le mois \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Ce champ doit être unique pour l'année \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Version non valide dans l'en-tête « Accept »."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Version non valide dans l'URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Version non valide dans le nom d'hôte."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Version non valide dans le paramètre de requête."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Ce compte est désactivé."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossible de se connecter avec les informations d'identification fournies."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"username\" et \"password\" doivent être inclus."
diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.mo b/rest_framework/locale/hu/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..451b0b9a
--- /dev/null
+++ b/rest_framework/locale/hu/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.po b/rest_framework/locale/hu/LC_MESSAGES/django.po
new file mode 100644
index 00000000..14fb6544
--- /dev/null
+++ b/rest_framework/locale/hu/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Zoltan Szalai <defaultdict@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Hungarian (http://www.transifex.com/projects/p/django-rest-framework/language/hu/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: hu\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Érvénytelen basic fejlécmező. Nem voltak megadva azonosítók."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Érvénytelen basic fejlécmező. Az azonosító karakterlánc nem tartalmazhat szóközöket."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Érvénytelen basic fejlécmező. Az azonosítók base64 kódolása nem megfelelő."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Érvénytelen felhasználónév/jelszó."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Érvénytelen token fejlécmező. Nem voltak megadva azonosítók."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Érvénytelen token fejlécmező. A token karakterlánc nem tartalmazhat szóközöket."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Érvénytelen token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "A felhasználó nincs aktiválva vagy törölve lett."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Szerver oldali hiba történt."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Hibás kérés."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Hibás azonosítók."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Nem voltak megadva azonosítók."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Nincs jogosultsága a művelet végrehajtásához."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nem található."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "A \"{method}\" metódus nem megengedett."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "A kérés Accept fejlécmezőjét nem lehetett kiszolgálni."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Nem támogatott média típus \"{media_type}\" a kérésben."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "A kérés korlátozva lett."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ennek a mezőnek a megadása kötelező."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ez a mező nem lehet null értékű."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "Az \"{input}\" nem egy érvényes logikai érték."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ez a mező nem lehet üres."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Bizonyosodjon meg arról, hogy ez a mező legfeljebb {max_length} karakterből áll."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Bizonyosodjon meg arról, hogy ez a mező legalább {min_length} karakterből áll."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Adjon meg egy érvényes e-mail címet!"
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ez az érték nem illeszkedik a szükséges mintázatra."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Az URL barát cím csak betűket, számokat, aláhúzásokat és kötőjeleket tartalmazhat."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Adjon meg egy érvényes URL-t!"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Egy érvényes egész szám megadása szükséges."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Bizonyosodjon meg arról, hogy ez az érték legfeljebb {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Bizonyosodjon meg arról, hogy ez az érték legalább {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "A karakterlánc túl hosszú."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Egy érvényes szám megadása szükséges."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Bizonyosodjon meg arról, hogy a számjegyek száma összesen legfeljebb {max_digits}."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Bizonyosodjon meg arról, hogy a tizedes tört törtrészében levő számjegyek száma összesen legfeljebb {max_decimal_places}."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Bizonyosodjon meg arról, hogy a tizedes tört egész részében levő számjegyek száma összesen legfeljebb {max_whole_digits}."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Időt is tartalmazó dátum helyett egy időt nem tartalmazó dátum lett elküldve."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Időt nem tartalmazó dátum helyett egy időt is tartalmazó dátum lett elküldve."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Az idő formátuma hibás. Használja ezek valamelyikét helyette: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "Az \"{input}\" nem egy érvényes elem."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Elemek listája helyett \"{input_type}\" lett elküldve."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Semmilyen fájl sem került feltöltésre."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Az elküldött adat nem egy fájl volt. Ellenőrizze a kódolás típusát az űrlapon!"
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "A fájlnév nem megállapítható."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "A küldött fájl üres."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Bizonyosodjon meg arról, hogy a fájlnév legfeljebb {max_length} karakterből áll (jelenlegi hossza: {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Töltsön fel egy érvényes képfájlt! A feltöltött fájl nem kép volt, vagy megsérült."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Érvénytelen oldal \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Érvénytelen pk \"{pk_value}\" - az objektum nem létezik."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Helytelen típus. pk érték helyett {data_type} lett elküldve."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Érvénytelen link - Nem illeszkedő URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Érvénytelen link. - Eltérő URL illeszkedés."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Érvénytelen link - Az objektum nem létezik."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Helytelen típus. URL karakterlánc helyett {data_type} lett elküldve."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Nem létezik olyan objektum, amelynél {slug_name}={value}."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Érvénytelen érték."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Érvénytelen adat. Egy dictionary helyett {datatype} lett elküldve."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ennek a mezőnek egyedinek kell lennie."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "A {field_names} mezőnevek nem tartalmazhatnak duplikátumot."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" dátumra."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" hónapra."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "A mezőnek egyedinek kell lennie a \"{date_field}\" évre."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Érvénytelen verzió az \"Accept\" fejlécmezőben."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Érvénytelen verzió az URL elérési útban."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Érvénytelen verzió a hosztnévben."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Érvénytelen verzió a lekérdezési paraméterben."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "A felhasználó tiltva van."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "A megadott azonosítókkal nem lehet bejelentkezni."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Tartalmaznia kell a \"felhasználónevet\" és a \"jelszót\"."
diff --git a/rest_framework/locale/id/LC_MESSAGES/django.mo b/rest_framework/locale/id/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..7fc98bda
--- /dev/null
+++ b/rest_framework/locale/id/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/id/LC_MESSAGES/django.po b/rest_framework/locale/id/LC_MESSAGES/django.po
new file mode 100644
index 00000000..99b70546
--- /dev/null
+++ b/rest_framework/locale/id/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Indonesian (http://www.transifex.com/projects/p/django-rest-framework/language/id/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: id\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/it/LC_MESSAGES/django.mo b/rest_framework/locale/it/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..82ceb810
--- /dev/null
+++ b/rest_framework/locale/it/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/it/LC_MESSAGES/django.po b/rest_framework/locale/it/LC_MESSAGES/django.po
new file mode 100644
index 00000000..2cdfb877
--- /dev/null
+++ b/rest_framework/locale/it/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mattia Procopio <promat85@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Italian (http://www.transifex.com/projects/p/django-rest-framework/language/it/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: it\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nome utente/password non validi"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Header del token non valido. Credenziali non fornite."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Header del token non valido. Il contenuto del token non dovrebbe contenere spazi."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token invalido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Utente inattivo o eliminato."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Errore del server."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Richiesta malformata."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenziali di autenticazione incorrette."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Non sono state immesse le credenziali di autenticazione."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Non hai l'autorizzazione per eseguire questa azione."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Non trovato."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metodo \"{method}\" non consentito"
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Impossibile soddisfare l'header \"Accept\" presente nella richiesta."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Tipo di media \"{media_type}\"non supportato."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Campo obbligatorio."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Il campo non puà essere nullo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" non è un valido valore booleano."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Questo campo non può essere omesso."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Assicurati che questo campo non abbia più di {max_length} caratteri."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Assicurati che questo campo abbia almeno {max_length} caratteri."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Inserisci un indirizzo email valido."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Immetti uno \"slug\" valido che consista di lettere, numeri, underscore o trattini."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Inserisci un URL valido"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "È richiesto un numero intero valido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Assicurati che il valore sia minore o uguale a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Assicurati che il valore sia maggiore o uguale a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "È richiesto un numero valido."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Assicurati che non ci siano più di {max_digits} cifre in totale."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Assicurati che non ci siano più di {max_decimal_places} cifre decimali."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Assicurati che non ci siano più di {max_whole_digits} cifre prima del separatore decimale."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "L'oggetto di tipo datetime è in un formato errato. Usa uno dei seguenti formati: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Atteso un oggetto di tipo datetime ma l'oggetto ricevuto è di tipo date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "La data è in un formato errato. Usa uno dei seguenti formati: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Atteso un oggetto di tipo date ma l'oggetto ricevuto è di tipo datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" non è una scelta valida."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Attesa una lista di oggetti ma l'oggetto ricevuto è di tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Non è stato inviato alcun file."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Il nome del file non può essere determinato."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Il file inviato è vuoto."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valore non valido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Questo campo deve essere unico."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "L'account dell'utente è disabilitato"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossibile eseguire il log in con le credenziali immesse."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Deve includere \"nome utente\" e \"password\"."
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..457bb53c
--- /dev/null
+++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
new file mode 100644
index 00000000..963fe89a
--- /dev/null
+++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# SUN CHOI <best2378@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Korean (Korea) (http://www.transifex.com/projects/p/django-rest-framework/language/ko_KR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ko_KR\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials) 문자열은 빈칸(spaces)을 포함하지 않아야 합니다."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 base64로 적절히 부호화(encode)되지 않았습니다."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "아이디/비밀번호가 유효하지 않습니다."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "토큰 헤더가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 빈칸(spaces)를 포함하지 않아야 합니다."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "토큰이 유효하지 않습니다."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "계정이 중지되었거나 삭제되었습니다."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "서버 장애가 발생했습니다."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "잘못된 요청입니다."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "자격 인증데이터(authentication credentials)가 정확하지 않습니다."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "자격 인증데이터(authentication credentials)가 제공되지 않았습니다."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "이 작업을 "
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "찾을 수 없습니다."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "메소드(Method) \"{method}\"는 허용되지 않습니다."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "요청된 \"{media_type}\"가 지원되지 않는 미디어 형태입니다."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "요청이 지연(throttled)되었습니다."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "이 항목을 채워주십시오."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\"이 유효하지 않은 부울(boolean)입니다."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "이 칸이 글자 수가 {max_length} 이하인지 확인하십시오."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "이 칸이 글자 수가 적어도 {min_length} 이상인지 확인하십시오."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "유효한 이메일 주소를 입력하십시오."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "형식에 맞지 않는 값입니다."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "문자, 숫자, 밑줄( _ ) 또는 하이픈( - )으로 이루어진 유효한 \"slug\"를 입력하십시오."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "유효한 URL을 입력하십시오."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "유효한 정수(integer)를 넣어주세요."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "이 값이 {max_value}보다 작거나 같은지 확인하십시오."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "이 값이 {min_value}보다 크거나 같은지 확인하십시오."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "문자열 값이 너무 큽니다."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "유효한 숫자를 넣어주세요."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "전체 숫자(digits)가 {max_digits} 이하인지 확인하십시오."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "소수점 자릿수가 {max_decimal_places} 이하인지 확인하십시오."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "소수점 자리 앞에 숫자(digits)가 {max_whole_digits} 이하인지 확인하십시오."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "예상된 datatime 대신 date를 받았습니다."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Date의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "예상된 date 대신 datetime을 받았습니다."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\"이 유효하지 않은 선택(choice)입니다."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "아이템 리스트가 예상되었으나 \"{input_type}\"를 받았습니다."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "파일이 제출되지 않았습니다."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "제출된 데이터는 파일이 아닙니다. 제출된 서식의 인코딩 형식을 확인하세요."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "파일명을 알 수 없습니다."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "제출된 파일이 비어있습니다."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "이 파일명의 글자수가 최대 {max_length}를 넘지 않는지 확인하십시오. (이것은 {length}가 있습니다)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "유효한 이미지 파일을 업로드 하십시오. 업로드 하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "유효하지 않은 page \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "유효하지 않은 pk \"{pk_value}\" - 객체가 존재하지 않습니다."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "잘못된 형식입니다. pk 값 대신 {data_type}를 받았습니다."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "유효하지 않은 하이퍼링크 - 일치하는 URL이 없습니다."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "유효하지 않은 하이퍼링크 - URL이 일치하지 않습니다."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "유효하지 않은 하이퍼링크 - 객체가 존재하지 않습니다."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "잘못된 형식입니다. URL 문자열을 예상했으나 {data_type}을 받았습니다."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "{slug_name}={value} 객체가 존재하지 않습니다."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "값이 유효하지 않습니다."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "유효하지 않은 데이터. 딕셔너리(dictionary)대신 {datatype}를 받았습니다."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "사용자 계정을 사용할 수 없습니다."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "제공된 인증데이터(credentials)로는 로그인할 수 없습니다."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"아이디\"와 \"비밀번호\"를 포함해야 합니다."
diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.mo b/rest_framework/locale/mk/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..fc586626
--- /dev/null
+++ b/rest_framework/locale/mk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.po b/rest_framework/locale/mk/LC_MESSAGES/django.po
new file mode 100644
index 00000000..d9a46953
--- /dev/null
+++ b/rest_framework/locale/mk/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Filip Dimitrovski <filipdimitrovski22@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Macedonian (http://www.transifex.com/projects/p/django-rest-framework/language/mk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: mk\n"
+"Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Невалиден основен header. Не се внесени податоци за автентикација."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Невалиден основен header. Автентикационата низа не треба да содржи празни места."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Невалиден основен header. Податоците за автентикација не се енкодирани со base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Невалидно корисничко име/лозинка."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Невалиден токен header. Не се внесени податоци за најава."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Невалиден токен во header. Токенот не треба да содржи празни места."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Невалиден токен."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Корисникот е деактивиран или избришан."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Настана серверска грешка."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Неправилен request."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Неточни податоци за најава."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Не се внесени податоци за најава."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Немате дозвола да го сторите ова."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Не е пронајдено ништо."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Методата \"{method}\" не е дозволена."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Не може да се исполни барањето на Accept header-от."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Media типот „{media_type}“ не е поддржан."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Request-от е забранет заради ограничувања."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Ова поле е задолжително."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Ова поле не смее да биде недефинирано."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" не е валиден boolean."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Ова поле не смее да биде празно."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Ова поле не смее да има повеќе од {max_length} знаци."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Ова поле мора да има барем {min_length} знаци."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Внесете валидна email адреса."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ова поле не е по правилната шема/барање."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Внесете валидно име што содржи букви, бројки, долни црти или црти."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Внесете валиден URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Задолжителен е валиден цел број."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Вредноста треба да биде помала или еднаква на {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Вредноста треба да биде поголема или еднаква на {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Вредноста е преголема."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Задолжителен е валиден број."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Не смее да има повеќе од {max_digits} цифри вкупно."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Не смее да има повеќе од {max_decimal_places} децимални места."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Не смее да има повеќе од {max_whole_digits} цифри пред децималната точка."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Датата и времето се со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Очекувано беше дата и време, а внесено беше само дата."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Датата е со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Очекувана беше дата, а внесени беа и дата и време."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Времето е со погрешен формат. Користете го овој формат: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "„{input}“ не е валиден избор."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Очекувана беше листа, а внесено беше „{input_type}“."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ниеден фајл не е качен (upload-иран)."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Испратените податоци не се фајл. Проверете го encoding-от на формата."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Не може да се открие име на фајлот."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Качениот (upload-иран) фајл е празен."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Името на фајлот треба да има највеќе {max_length} знаци (а има {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Качете (upload-ирајте) валидна слика. Фајлот што го качивте не е валидна слика или е расипан."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Невалидна страна „{page_number}“: {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Невалиден pk „{pk_value}“ - објектот не постои."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Неточен тип. Очекувано беше pk, а внесено {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Невалиден хиперлинк - не е внесен URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Невалиден хиперлинк - внесен е неправилен URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Невалиден хиперлинк - Објектот не постои."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Неточен тип. Очекувано беше URL, a внесено {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Објектот со {slug_name}={value} не постои."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Невалидна вредност."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Невалидни податоци. Очекуван беше dictionary, а внесен {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Ова поле мора да биде уникатно."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Полињата {field_names} заедно мора да формираат уникатен збир."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ датата."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ месецот."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Ова поле мора да биде уникатно за „{date_field}“ годината."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Невалидна верзија во „Accept“ header-от."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Невалидна верзија во URL патеката."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Невалидна верзија во hostname-от."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Невалидна верзија во query параметарот."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Сметката на корисникот е деактивирана."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Не може да се најавите со податоците за најава."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Мора да се внесе „username“ и „password“."
diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.mo b/rest_framework/locale/nl/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..b0e1ad77
--- /dev/null
+++ b/rest_framework/locale/nl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.po b/rest_framework/locale/nl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..a1215512
--- /dev/null
+++ b/rest_framework/locale/nl/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Dutch (http://www.transifex.com/projects/p/django-rest-framework/language/nl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: nl\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.mo b/rest_framework/locale/pl/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..9db72cfb
--- /dev/null
+++ b/rest_framework/locale/pl/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.po b/rest_framework/locale/pl/LC_MESSAGES/django.po
new file mode 100644
index 00000000..8e51d754
--- /dev/null
+++ b/rest_framework/locale/pl/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Janusz Harkot <jh@blueice.pl>, 2015
+# Maciek Olko <maciej.olko@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Polish (http://www.transifex.com/projects/p/django-rest-framework/language/pl/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pl\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Niepoprawny podstawowy nagłówek. Brak danych uwierzytelniających."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Niepoprawny podstawowy nagłówek. Ciąg znaków danych uwierzytelniających nie powinien zawierać spacji."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Niepoprawny podstawowy nagłówek. Niewłaściwe kodowanie base64 danych uwierzytelniających."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Niepoprawna nazwa użytkownika lub hasło."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Niepoprawny nagłówek tokena. Brak danych uwierzytelniających."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Niepoprawny nagłówek tokena. Token nie może zawierać odstępów."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Niepoprawny token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Użytkownik nieaktywny lub usunięty."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Wystąpił błąd serwera."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Zniekształcone żądanie."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Błędne dane uwierzytelniające."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Nie podano danych uwierzytelniających."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Nie masz uprawnień, by wykonać tę czynność."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nie znaleziono."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Niedozwolona metoda \"{method}\"."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nie można zaspokoić nagłówka Accept żądania."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Brak wsparcia dla żądanego typu danych \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Żądanie zostało zdławione."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "To pole jest wymagane."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Pole nie może mieć wartości null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" nie jest poprawną wartością logiczną."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "To pole nie może być puste."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Upewnij się, że to pole ma nie więcej niż {max_length} znaków."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Upewnij się, że pole ma co najmniej {min_length} znaków."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Podaj poprawny adres e-mail."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Ta wartość nie pasuje do wymaganego wzorca."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Wprowadź poprawną wartość pola typu \"slug\", składającą się ze znaków łacińskich, cyfr, podkreślenia lub myślnika."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Wprowadź poprawny adres URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Wymagana poprawna liczba całkowita."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Upewnij się, że ta wartość jest mniejsza lub równa {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Upewnij się, że ta wartość jest większa lub równa {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Za długi ciąg znaków."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Wymagana poprawna liczba."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_digits} cyfr."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_decimal_places} cyfr dziesiętnych."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Upewnij się, że liczba ma nie więcej niż {max_whole_digits} cyfr całkowitych."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Wartość daty z czasem ma zły format. Użyj jednego z dostępnych formatów: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Oczekiwano datę z czasem, otrzymano tylko datę."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Data ma zły format. Użyj jednego z tych formatów: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Oczekiwano daty a otrzymano datę z czasem."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Błędny format czasu. Użyj jednego z dostępnych formatów: {format}"
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" nie jest poprawnym wyborem."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Oczekiwano listy elementów, a otrzymano dane typu \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Nie przesłano pliku."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Przesłane dane nie były plikiem. Sprawdź typ kodowania formatki."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nie można określić nazwy pliku."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Przesłany plik jest pusty."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Upewnij się, że nazwa pliku ma długość co najwyżej {max_length} znaków (aktualnie ma {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Prześlij poprawny plik graficzny. Przesłany plik albo nie jest grafiką lub jest uszkodzony."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Niepoprawna strona \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Błędny klucz główny \"{pk_value}\" - obiekt nie istnieje."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Błędny typ danych. Oczekiwano wartość klucza głównego, otrzymano {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Błędny hyperlink - nie znaleziono pasującego adresu URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Błędny hyperlink - błędne dopasowanie adresu URL."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Błędny hyperlink - obiekt nie istnieje."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Błędny typ danych. Oczekiwano adresu URL, otrzymano {data_type}"
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Obiekt z polem {slug_name}={value} nie istnieje"
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Niepoprawna wartość."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Niepoprawne dane. Oczekiwano słownika, otrzymano {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Wartość dla tego pola musi być unikalna."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Pola {field_names} muszą tworzyć unikalny zestaw."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "To pole musi mieć unikalną wartość dla jednej daty z pola \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "To pole musi mieć unikalną wartość dla konkretnego miesiąca z pola \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "To pole musi mieć unikalną wartość dla konkretnego roku z pola \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Błędna wersja w nagłówku \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Błędna wersja w ścieżce URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Błędna wersja w nazwie hosta."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Błędna wersja w parametrach zapytania."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Konto użytkownika jest nieaktywne."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Podane dane uwierzytelniające nie pozwalają na zalogowanie."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musi zawierać \"username\" i \"password\"."
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..49f01929
--- /dev/null
+++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
new file mode 100644
index 00000000..3f272f71
--- /dev/null
+++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po
@@ -0,0 +1,326 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Craig Blaszczyk <masterjakul@gmail.com>, 2015
+# Filipe Rinaldi <filipe.rinaldi@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-rest-framework/language/pt_BR/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: pt_BR\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Cabeçalho básico inválido. Credenciais não fornecidas."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Cabeçalho básico inválido. String de credenciais não deve incluir espaços."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Cabeçalho básico inválido. Credenciais codificadas em base64 incorretamente."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Usário ou senha inválido."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Cabeçalho de token inválido. Credenciais não fornecidas."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Cabeçalho de token inválido. String de token não deve incluir espaços."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Token inválido."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Usuário inativo ou removido."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ocorreu um erro de servidor."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Pedido malformado."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Credenciais de autenticação incorretas."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "As credenciais de autenticação não foram fornecidas."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Você não tem persmissao para executar essa ação."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Não encontrado."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Método \"{method}\" não é permitido."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Não foi possível satisfazer a requisição do cabeçalho Accept."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Media type \"{media_type}\" no pedido não é suportado."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Pedido foi limitado."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Este campo é obrigatório."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Este campo não pode ser nulo."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" não é um valor boleano válido."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Este campo não pode ser em branco."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Certifique-se de que este campo não tenha mais de {max_length} caracteres."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Certifique-se de que este campo tenha mais de {min_length} caracteres."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Insira um endereço de email válido."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Este valor não corresponde ao padrão exigido."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Entrar um \"slug\" válido que consista de letras, números, sublinhados ou hífens."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Entrar um URL válido."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Um número inteiro válido é exigido."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Certifique-se de que este valor seja inferior ou igual a {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Certifque-se de que este valor seja maior ou igual a {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Valor da string é muito grande."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Um número válido é necessário."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Certifique-se de que não haja mais de {max_digits} dígitos no total."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Certifique-se de que não haja mais de {max_decimal_places} casas decimais."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Certifique-se de que não haja mais de {max_whole_digits} dígitos antes do ponto decimal."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Formato inválido para data e hora. Use um dos formatos a seguir: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Data e hora são necessários mas apenas data foi encontrada."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Formato inválido para data. Use um dos formatos a seguir: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Necessário uma data mas recebeu uma data e hora."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Tempo tem formato errado. Usa um desses em vez disso: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" não é um escolha válido."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ficheiro não foi submetido."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Os dados submetidos nao foram um ficheiro. Certifique-se do tipo de codificação no formulário."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Nome do arquivo não pode ser determinado."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "O arquivo submetido ésta vázio."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Certifique-se de que o nome do ficheiro tem menos de {max_length} caracteres (tem {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Fazer upload de um imagem válido. O arquivo mandou não foi um imagem ou foi corrupto."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Página inválido \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Pk inválido \"{pk_value}\" - objeto não existe."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Tipo incorreto. Necessário valor pk, recebeu {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Hyperlink inválido - URL não combinou."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Hyperlink inválido - URL combinou errado."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Hyperlink inválido - objeto não existe."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Tipo incorreto. Necessário string URL, recebeu {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objeto com {slug_name}={value} não existe."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Valor inválido."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Data inválido. Necessário um dicionário mas recebeu {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Esse campo deve ser unico."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Os campos {field_names} devem criar um set unico."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "O campo deve ser unico pela data \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "O campo deve ser unico pelo anô \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "O campo deve ser unico pela mês \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Versão inválido no cabeçalho \"Accept\"."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Versão inválido no caminho de URL."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Versão inválido no hostname."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Versão inválida no parâmetro de query."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Conta de usário desabilitada."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Impossível fazer login com as credenciais fornecidas."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Obrigatório incluir \"usuário\" e \"senha\"."
diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.mo b/rest_framework/locale/ru/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..d1555f1f
--- /dev/null
+++ b/rest_framework/locale/ru/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.po b/rest_framework/locale/ru/LC_MESSAGES/django.po
new file mode 100644
index 00000000..38489747
--- /dev/null
+++ b/rest_framework/locale/ru/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Mikhail Dmitriev <mktums@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Russian (http://www.transifex.com/projects/p/django-rest-framework/language/ru/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: ru\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Недопустимый заголовок. Не предоставлены учетные данные."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Недопустимый заголовок. Учетные данные не должны содержать пробелов."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Недопустимый заголовок. Учетные данные некорректно закодированны в base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Недопустимые имя пользователя или пароль."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Недопустимый заголовок токена. Не предоставлены учетные данные."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Недопустимый заголовок токена. Токен не должен содержать пробелов."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Недопустимый токен."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Пользователь неактивен или удален."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Произошла ошибка сервера."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Искаженный запрос."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Некорректные учетные данные."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Учетные данные не были предоставлены."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "У вас нет прав для выполнения этой операции."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Не найдено."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Метод \"{method}\" не разрешен."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Невозможно удовлетворить \"Accept\" заголовок запроса."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Неподдерживаемый тип данных \"{media_type}\" в запросе."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Запрос был проигнорирован."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Это поле обязательно."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Это поле не может быть null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" не является корректным булевым значением."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Это поле не может быть пустым."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Убедитесь что в этом поле не больше {max_length} символов."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Убедитесь что в этом поле как минимум {min_length} символов."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Введите корректный адрес электронной почты."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Значение не соответствует требуемому паттерну."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Введите корректный \"slug\", состоящий из букв, цифр, знаков подчеркивания или дефисов."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Введите корректный URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Требуется целочисленное значение."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Убедитесь что значение меньше или равно {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Убедитесь что значение больше или равно {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Слишком длинное значение."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Требуется численное значение."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Убедитесь что в числе не больше {max_digits} знаков."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Убедитесь что в числе не больше {max_decimal_places} знаков в дробной части."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Убедитесь что в цисле не больше {max_whole_digits} знаков в целой части."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат datetime. Используйте один из этих форматов: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Ожидался datetime, но был получен date."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат date. Используйте один из этих форматов: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Ожидался date, но был получен datetime."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Неправильный формат времени. Используйте один из этих форматов: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" не является корректным значением."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Ожидался list со значениями, но был получен \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Не был загружен файл."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Загруженный файл не является корректным файлом. "
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Невозможно определить имя файла."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Загруженный файл пуст."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Убедитесь что имя файла меньше {max_length} символов (сейчас {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Загрузите корректное изображение. Загруженный файл не является изображением, либо является испорченным."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Недопустимая страница \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Недопустимый первичный ключ \"{pk_value}\" - объект не существует."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Некорректный тип. Ожилалось значение первичного ключа, получен {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Недопустимая ссылка - нет совпадения по URL."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Недопустимая ссылка - некорректное совпадение по URL,"
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Недопустимая ссылка - объект не существует."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Некорректный тип. Ожидался URL, получен {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Объект с {slug_name}={value} не существует."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Недопустимое значение."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Недопустимые данные. Ожидался dictionary, но был получен {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Учетная запись пользователя отключена."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Невозможно войти с предоставленными учетными данными."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Должен включать \"username\" и \"password\"."
diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.mo b/rest_framework/locale/sk/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..53bb95d8
--- /dev/null
+++ b/rest_framework/locale/sk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.po b/rest_framework/locale/sk/LC_MESSAGES/django.po
new file mode 100644
index 00000000..9dd378c0
--- /dev/null
+++ b/rest_framework/locale/sk/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Stanislav Komanec <stano@videoflot.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Slovak (http://www.transifex.com/projects/p/django-rest-framework/language/sk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sk\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Nesprávna hlavička. Neboli poskytnuté prihlasovacie údaje."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Nesprávna hlavička. Prihlasovacie údaje nesmú obsahovať medzery."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Nesprávna hlavička. Prihlasovacie údaje nie sú správne zakódované pomocou metódy base64."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Nesprávne prihlasovacie údaje."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Nesprávna token hlavička. Neboli poskytnuté prihlasovacie údaje."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Nesprávna token hlavička. Token hlavička nesmie obsahovať medzery."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Nesprávny token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Daný používateľ je neaktívny, alebo zmazaný."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Vyskytla sa chyba na strane servera."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Požiadavok má nesprávny formát, alebo je poškodený."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Nesprávne prihlasovacie údaje."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Prihlasovacie údaje neboli zadané."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "K danej akcii nemáte oprávnenie."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Nebolo nájdené."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metóda \"{method}\" nie je povolená."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Nie je možné vyhovieť požiadavku v hlavičke \"Accept\"."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Požiadavok obsahuje nepodporovaný media type: \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Daný používateľ je zablokovaný."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "S danými prihlasovacími údajmi nebolo možné sa prihlásiť."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Musí obsahovať parametre \"používateľské meno\" a \"heslo\"."
diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.mo b/rest_framework/locale/sv/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..a33b0cc5
--- /dev/null
+++ b/rest_framework/locale/sv/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.po b/rest_framework/locale/sv/LC_MESSAGES/django.po
new file mode 100644
index 00000000..1602bf55
--- /dev/null
+++ b/rest_framework/locale/sv/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Joakim Soderlund, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Swedish (http://www.transifex.com/projects/p/django-rest-framework/language/sv/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: sv\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "Ogiltig \"basic\"-header. Inga användaruppgifter tillhandahölls."
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "Ogiltig \"basic\"-header. Strängen för användaruppgifterna ska inte innehålla mellanslag."
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "Ogiltig \"basic\"-header. Användaruppgifterna är inte korrekt base64-kodade."
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Ogiltigt användarnamn/lösenord."
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Ogiltig \"token\"-header. Inga användaruppgifter tillhandahölls."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Ogiltig \"token\"-header. Strängen för referensen ska inte innehålla mellanslag."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Ogiltig \"token\"."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Användaren borttagen eller inaktiv."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Ett serverfel inträffade."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Ogiltig förfrågan."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Ogiltiga inloggningsuppgifter. "
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Autentiseringsuppgifter ej tillhandahållna."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Du har inte tillåtelse att utföra denna förfrågan."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Hittades inte."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "Metoden \"{method}\" tillåts inte."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "Kunde inte tillfredsställa förfrågans \"Accept\"-header."
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "Medietypen \"{media_type}\" stöds inte."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "Förfrågan stoppades eftersom du har skickat för många."
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Det här fältet är obligatoriskt."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Det här fältet får inte vara null."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" är inte ett giltigt booleskt värde."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Det här fältet får inte vara blankt."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Se till att detta fält inte har fler än {max_length} tecken."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Se till att detta fält har minst {min_length} tecken."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Ange en giltig mejladress."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Det här värdet matchar inte mallen."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Ange en giltig \"slug\" bestående av bokstäver, nummer, understreck eller bindestreck."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Ange en giltig URL."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Ett giltigt heltal krävs."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Se till att detta värde är mindre än eller lika med {max_value}."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Se till att detta värde är större än eller lika med {min_value}."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "Textvärdet är för långt."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Ett giltigt nummer krävs."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Se till att det inte finns fler än totalt {max_digits} siffror."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Se till att det inte finns fler än {max_decimal_places} decimaler."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Se till att det inte finns fler än {max_whole_digits} siffror före decimalpunkten."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datumtiden har fel format. Använd ett av dessa format istället: {format}."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Förväntade en datumtid men fick ett datum."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Datumet har fel format. Använde ett av dessa format istället: {format}."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Förväntade ett datum men fick en datumtid."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Tiden har fel format. Använd ett av dessa format istället: {format}."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" är inte ett giltigt val."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Förväntade en lista med element men fick typen \"{input_type}\"."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Ingen fil skickades."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Den skickade informationen var inte en fil. Kontrollera formulärets kodningstyp."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Inget filnamn kunde bestämmas."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Den skickade filen var tom."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Se till att det här filnamnet har högst {max_length} tecken (det har {length})."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild eller en skadad bild."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Ogiltigt sida \"{page_number}\": {message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Ogiltigt pk \"{pk_value}\" - Objektet finns inte."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Felaktig typ. Förväntade pk-värde, fick {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Ogiltig hyperlänk - Ingen URL matchade."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Ogiltig hyperlänk - Felaktig URL-matching."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Ogiltig hyperlänk - Objektet finns inte."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Felaktig typ. Förväntade URL-sträng, fick {data_type}."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "Objekt med {slug_name}={value} finns inte."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Ogiltigt värde."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Ogiltig data. Förväntade en dictionary, men fick {datatype}."
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Det här fältet måste vara unikt."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "Fälten {field_names} måste skapa ett unikt set."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Det här fältet måste vara unikt för datumet \"{date_field}\"."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Det här fältet måste vara unikt för månaden \"{date_field}\"."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Det här fältet måste vara unikt för året \"{date_field}\"."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "Ogiltig version i \"Accept\"-headern."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "Ogiltig version i URL-resursen."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Ogiltig version i värdnamnet."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Ogiltig version i förfrågningsparametern."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Användarkontot är borttaget."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Kunde inte logga in med de angivna inloggningsuppgifterna."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "Användarnamn och lösenord måste anges."
diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.mo b/rest_framework/locale/tr/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..e6b848cf
--- /dev/null
+++ b/rest_framework/locale/tr/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.po b/rest_framework/locale/tr/LC_MESSAGES/django.po
new file mode 100644
index 00000000..5aabbeba
--- /dev/null
+++ b/rest_framework/locale/tr/LC_MESSAGES/django.po
@@ -0,0 +1,328 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Ertaç Paprat <epaprat@gmail.com>, 2015
+# Mesut Can Gürle <mesutcang@gmail.com>, 2015
+# Recep KIRMIZI <rkirmizi@gmail.com>, 2015
+# Ülgen Sarıkavak <ulgensrkvk@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Turkish (http://www.transifex.com/projects/p/django-rest-framework/language/tr/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: tr\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "Geçersiz kullanıcı adı/parola"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "Geçersiz token başlığı. Kimlik bilgileri eksik."
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "Geçersiz token başlığı. Token'da boşluk olmamalı."
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "Geçersiz token."
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "Kullanıcı aktif değil ya da silinmiş."
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "Sunucu hatası oluştu."
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "Bozuk istek."
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "Giriş bilgileri hatalı."
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "Giriş bilgileri verilmedi."
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "Bu işlemi yapmak için izniniz bulunmuyor."
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "Bulunamadı."
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "\"{method}\" metoduna izin verilmiyor."
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "İstekte desteklenmeyen medya tipi: \"{media_type}\"."
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "Bu alan zorunlu."
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "Bu alan boş bırakılmamalı."
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "\"{input}\" geçerli bir boolean değil."
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "Bu alan boş bırakılmamalı."
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "Bu alanın {max_length} karakterden fazla karakter barındırmadığından emin olun."
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "Bu alanın en az {min_length} karakter barındırdığından emin olun."
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "Geçerli bir e-posta adresi girin."
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "Bu değer gereken düzenli ifade deseni ile uyuşmuyor."
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "Harf, rakam, altçizgi veya tireden oluşan geçerli bir \"slug\" giriniz."
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "Geçerli bir URL girin."
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "Geçerli bir tam sayı girin."
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "Değerin {max_value} değerinden küçük ya da eşit olduğundan emin olun."
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "Değerin {min_value} değerinden büyük ya da eşit olduğundan emin olun."
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "String değeri çok uzun."
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "Geçerli bir numara gerekiyor."
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "Toplamda {max_digits} haneden fazla hane olmadığından emin olun."
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "Ondalık basamak değerinin {max_decimal_places} haneden fazla olmadığından emin olun."
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "Ondalık ayracından önce {max_whole_digits} basamaktan fazla olmadığından emin olun."
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "Datetime alanı yanlış biçimde. {format} biçimlerinden birini kullanın."
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "Datetime değeri bekleniyor, ama date değeri geldi."
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "Tarih biçimi yanlış. {format} biçimlerinden birini kullanın."
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "Date tipi beklenmekteydi, fakat datetime tipi geldi."
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "Time biçimi yanlış. {format} biçimlerinden birini kullanın."
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "\"{input}\" geçerli bir seçim değil."
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "Elemanların listesi beklenirken \"{input_type}\" alındı."
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "Hiçbir dosya verilmedi."
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "Gönderilen veri dosya değil. Formdaki kodlama tipini kontrol edin."
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "Hiçbir dosya adı belirlenemedi."
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "Gönderilen dosya boş."
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "Bu dosya adının en fazla {max_length} karakter uzunluğunda olduğundan emin olun. (şu anda {length} karakter)."
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "Geçerli bir resim yükleyin. Yüklediğiniz dosya resim değil ya da bozuk."
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "Geçersiz sayfa \"{page_number}\":{message}."
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "Geçersiz pk \"{pk_value}\" - obje bulunamadı."
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "Hatalı tip. Pk değeri beklenirken, alınan {data_type}."
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "Geçersiz bağlantı - Hiçbir URL eşleşmedi."
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "Geçersiz bağlantı - Yanlış URL eşleşmesi."
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "Geçersiz bağlantı - Obje bulunamadı."
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "Hatalı tip. URL metni bekleniyor, {data_type} alındı."
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "{slug_name}={value} değerini taşıyan obje bulunamadı."
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "Geçersiz değer."
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "Geçersiz veri. Sözlük bekleniyordu fakat {datatype} geldi. "
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "Bu alan eşsiz olmalı."
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "{field_names} hep birlikte eşsiz bir küme oluşturmalılar."
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "Bu alan \"{date_field}\" tarihine göre eşsiz olmalı."
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "Bu alan \"{date_field}\" ayına göre eşsiz olmalı."
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "Bu alan \"{date_field}\" yılına göre eşsiz olmalı."
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "\"Accept\" başlığındaki sürüm geçersiz."
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "URL dizininde geçersiz versiyon."
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "Host adında geçersiz versiyon."
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "Sorgu parametresinde geçersiz versiyon."
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "Kullanıcı hesabı devre dışı bırakılmış."
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "Verilen bilgiler ile giriş sağlanamadı."
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "\"Kullanıcı Adı\" ve \"Parola\" eklenmeli."
diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.mo b/rest_framework/locale/uk/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..fc335054
--- /dev/null
+++ b/rest_framework/locale/uk/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.po b/rest_framework/locale/uk/LC_MESSAGES/django.po
new file mode 100644
index 00000000..93fc2bf9
--- /dev/null
+++ b/rest_framework/locale/uk/LC_MESSAGES/django.po
@@ -0,0 +1,324 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Ukrainian (http://www.transifex.com/projects/p/django-rest-framework/language/uk/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: uk\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr ""
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr ""
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr ""
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr ""
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr ""
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr ""
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr ""
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr ""
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr ""
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr ""
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr ""
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr ""
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr ""
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr ""
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr ""
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr ""
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr ""
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr ""
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr ""
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr ""
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr ""
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr ""
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr ""
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr ""
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr ""
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr ""
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr ""
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr ""
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr ""
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr ""
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr ""
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr ""
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr ""
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr ""
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr ""
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr ""
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr ""
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr ""
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr ""
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr ""
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr ""
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr ""
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr ""
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr ""
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr ""
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr ""
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr ""
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr ""
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr ""
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr ""
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr ""
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr ""
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr ""
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr ""
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr ""
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr ""
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr ""
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr ""
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr ""
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr ""
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr ""
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr ""
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr ""
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr ""
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr ""
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr ""
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr ""
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr ""
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr ""
diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo
new file mode 100644
index 00000000..6e7073bd
--- /dev/null
+++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo
Binary files differ
diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
new file mode 100644
index 00000000..01128859
--- /dev/null
+++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po
@@ -0,0 +1,325 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+#
+# Translators:
+# Lele Long <schemacs@gmail.com>, 2015
+msgid ""
+msgstr ""
+"Project-Id-Version: Django REST framework\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2015-01-30 16:23+0000\n"
+"PO-Revision-Date: 2015-01-30 16:27+0000\n"
+"Last-Translator: Thomas Christie <tom@tomchristie.com>\n"
+"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-rest-framework/language/zh_CN/)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Language: zh_CN\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+
+#: authentication.py:69
+msgid "Invalid basic header. No credentials provided."
+msgstr "没有提供认证信息(基本认证HTTP头无效)。"
+
+#: authentication.py:72
+msgid "Invalid basic header. Credentials string should not contain spaces."
+msgstr "认证字符串不应该包含空格(基本认证HTTP头无效)。"
+
+#: authentication.py:78
+msgid "Invalid basic header. Credentials not correctly base64 encoded."
+msgstr "认证字符串base64编码错误(基本认证HTTP头无效)。"
+
+#: authentication.py:90
+msgid "Invalid username/password."
+msgstr "用户名或者密码错误。"
+
+#: authentication.py:156
+msgid "Invalid token header. No credentials provided."
+msgstr "没有提供认证信息(认证令牌HTTP头无效)。"
+
+#: authentication.py:159
+msgid "Invalid token header. Token string should not contain spaces."
+msgstr "认证令牌字符串不应该包含空格(无效的认证令牌HTTP头)。"
+
+#: authentication.py:168
+msgid "Invalid token."
+msgstr "认证令牌无效。"
+
+#: authentication.py:171
+msgid "User inactive or deleted."
+msgstr "用户未激活或者已删除。"
+
+#: exceptions.py:38
+msgid "A server error occurred."
+msgstr "服务器出现了错误。"
+
+#: exceptions.py:73
+msgid "Malformed request."
+msgstr "畸形的请求。"
+
+#: exceptions.py:78
+msgid "Incorrect authentication credentials."
+msgstr "不正确的身份认证凭据。"
+
+#: exceptions.py:83
+msgid "Authentication credentials were not provided."
+msgstr "身份认证凭据未提供。"
+
+#: exceptions.py:88
+msgid "You do not have permission to perform this action."
+msgstr "您没有执行该操作的权限。"
+
+#: exceptions.py:93
+msgid "Not found."
+msgstr "未找到。"
+
+#: exceptions.py:98
+msgid "Method \"{method}\" not allowed."
+msgstr "方法 “{method}” 不被允许。"
+
+#: exceptions.py:109
+msgid "Could not satisfy the request Accept header."
+msgstr "无法满足Accept HTTP头的请求。"
+
+#: exceptions.py:121
+msgid "Unsupported media type \"{media_type}\" in request."
+msgstr "不支持请求中的媒体类型 “{media_type}”。"
+
+#: exceptions.py:134
+msgid "Request was throttled."
+msgstr "请求被限速。"
+
+#: fields.py:153 relations.py:132 relations.py:156 validators.py:77
+#: validators.py:155
+msgid "This field is required."
+msgstr "这个字段是必填项。"
+
+#: fields.py:154
+msgid "This field may not be null."
+msgstr "这个值不能为 null。"
+
+#: fields.py:487 fields.py:515
+msgid "\"{input}\" is not a valid boolean."
+msgstr "“{input}” 不是合法的布尔值。"
+
+#: fields.py:550
+msgid "This field may not be blank."
+msgstr "此字段不能为空。"
+
+#: fields.py:551 fields.py:1324
+msgid "Ensure this field has no more than {max_length} characters."
+msgstr "请确保这个字段不能超过 {max_length} 个字符。"
+
+#: fields.py:552
+msgid "Ensure this field has at least {min_length} characters."
+msgstr "请确保这个字段至少包含 {min_length} 个字符。"
+
+#: fields.py:587
+msgid "Enter a valid email address."
+msgstr "请输入合法的邮件地址。"
+
+#: fields.py:604
+msgid "This value does not match the required pattern."
+msgstr "输入值不匹配要求的模式。"
+
+#: fields.py:615
+msgid ""
+"Enter a valid \"slug\" consisting of letters, numbers, underscores or "
+"hyphens."
+msgstr "请输入合法的“短语“,只能包含字母,数字,下划线或者中划线。"
+
+#: fields.py:627
+msgid "Enter a valid URL."
+msgstr "请输入合法的URL。"
+
+#: fields.py:638
+msgid "\"{value}\" is not a valid UUID."
+msgstr ""
+
+#: fields.py:657
+msgid "A valid integer is required."
+msgstr "请填写合法的整数值。"
+
+#: fields.py:658 fields.py:692 fields.py:725
+msgid "Ensure this value is less than or equal to {max_value}."
+msgstr "请确保该值小于或者等于 {max_value}。"
+
+#: fields.py:659 fields.py:693 fields.py:726
+msgid "Ensure this value is greater than or equal to {min_value}."
+msgstr "请确保该值大于或者等于 {min_value}。"
+
+#: fields.py:660 fields.py:694 fields.py:730
+msgid "String value too large."
+msgstr "字符值太长。"
+
+#: fields.py:691 fields.py:724
+msgid "A valid number is required."
+msgstr "请填写合法的数字。"
+
+#: fields.py:727
+msgid "Ensure that there are no more than {max_digits} digits in total."
+msgstr "请确保总计不超过 {max_digits} 个数字。"
+
+#: fields.py:728
+msgid ""
+"Ensure that there are no more than {max_decimal_places} decimal places."
+msgstr "请确保总计不超过 {max_decimal_places} 个小数位。"
+
+#: fields.py:729
+msgid ""
+"Ensure that there are no more than {max_whole_digits} digits before the "
+"decimal point."
+msgstr "请确保小数点前不超过 {max_whole_digits} 个数字。"
+
+#: fields.py:813
+msgid "Datetime has wrong format. Use one of these formats instead: {format}."
+msgstr "日期时间格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:814
+msgid "Expected a datetime but got a date."
+msgstr "期望为日期时间,得到的是日期。"
+
+#: fields.py:878
+msgid "Date has wrong format. Use one of these formats instead: {format}."
+msgstr "日期格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:879
+msgid "Expected a date but got a datetime."
+msgstr "期望为日期,得到的是日期时间。"
+
+#: fields.py:936
+msgid "Time has wrong format. Use one of these formats instead: {format}."
+msgstr "时间格式错误。请从这些格式中选择:{format}。"
+
+#: fields.py:992 fields.py:1036
+msgid "\"{input}\" is not a valid choice."
+msgstr "“{input}” 不是合法选项。"
+
+#: fields.py:1037 fields.py:1151 serializers.py:482
+msgid "Expected a list of items but got type \"{input_type}\"."
+msgstr "期望为一个包含物件的列表,得到的类型是“{input_type}”。"
+
+#: fields.py:1067
+msgid "No file was submitted."
+msgstr "没有提交任何文件。"
+
+#: fields.py:1068
+msgid ""
+"The submitted data was not a file. Check the encoding type on the form."
+msgstr "提交的数据不是一个文件。请检查表单的编码类型。"
+
+#: fields.py:1069
+msgid "No filename could be determined."
+msgstr "无法检测到文件名。"
+
+#: fields.py:1070
+msgid "The submitted file is empty."
+msgstr "提交的是空文件。"
+
+#: fields.py:1071
+msgid ""
+"Ensure this filename has at most {max_length} characters (it has {length})."
+msgstr "确保该文件名最多包含 {max_length} 个字符 ( 当前长度为{length} ) 。"
+
+#: fields.py:1113
+msgid ""
+"Upload a valid image. The file you uploaded was either not an image or a "
+"corrupted image."
+msgstr "请上传有效图片。您上传的该文件不是图片或者图片已经损坏。"
+
+#: fields.py:1188
+msgid "Expected a dictionary of items but got type \"{input_type}\"."
+msgstr ""
+
+#: pagination.py:221
+msgid "Invalid page \"{page_number}\": {message}."
+msgstr "无效页面 “{page_number}”:{message}。"
+
+#: pagination.py:442
+msgid "Invalid cursor"
+msgstr ""
+
+#: relations.py:133
+msgid "Invalid pk \"{pk_value}\" - object does not exist."
+msgstr "无效主键 “{pk_value}” - 对象不存在。"
+
+#: relations.py:134
+msgid "Incorrect type. Expected pk value, received {data_type}."
+msgstr "类型错误。期望为主键,得到的类型为 {data_type}。"
+
+#: relations.py:157
+msgid "Invalid hyperlink - No URL match."
+msgstr "无效超链接 -没有匹配的URL。"
+
+#: relations.py:158
+msgid "Invalid hyperlink - Incorrect URL match."
+msgstr "无效超链接 -错误的URL匹配。"
+
+#: relations.py:159
+msgid "Invalid hyperlink - Object does not exist."
+msgstr "无效超链接 -对象不存在。"
+
+#: relations.py:160
+msgid "Incorrect type. Expected URL string, received {data_type}."
+msgstr "类型错误。期望为URL字符串,得到的类型是 {data_type}。"
+
+#: relations.py:295
+msgid "Object with {slug_name}={value} does not exist."
+msgstr "属性 {slug_name} 为 {value} 的对象不存在。"
+
+#: relations.py:296
+msgid "Invalid value."
+msgstr "无效值。"
+
+#: serializers.py:299
+msgid "Invalid data. Expected a dictionary, but got {datatype}."
+msgstr "无效数据。期待为字典类型,得到的是 {datatype} 。"
+
+#: validators.py:22
+msgid "This field must be unique."
+msgstr "该字段必须唯一。"
+
+#: validators.py:76
+msgid "The fields {field_names} must make a unique set."
+msgstr "字段 {field_names} 必须能构成唯一集合。"
+
+#: validators.py:219
+msgid "This field must be unique for the \"{date_field}\" date."
+msgstr "该字段必须在日期 “{date_field}” 唯一。"
+
+#: validators.py:234
+msgid "This field must be unique for the \"{date_field}\" month."
+msgstr "该字段必须在月份 “{date_field}” 唯一。"
+
+#: validators.py:247
+msgid "This field must be unique for the \"{date_field}\" year."
+msgstr "该字段必须在年 “{date_field}” 唯一。"
+
+#: versioning.py:39
+msgid "Invalid version in \"Accept\" header."
+msgstr "“Accept” HTTP头包含无效版本。"
+
+#: versioning.py:70 versioning.py:112
+msgid "Invalid version in URL path."
+msgstr "URl路径包含无效版本。"
+
+#: versioning.py:138
+msgid "Invalid version in hostname."
+msgstr "主机名包含无效版本。"
+
+#: versioning.py:160
+msgid "Invalid version in query parameter."
+msgstr "请求参数里包含无效版本。"
+
+#: authtoken/serializers.py:20
+msgid "User account is disabled."
+msgstr "用户账户已禁用。"
+
+#: authtoken/serializers.py:23
+msgid "Unable to log in with provided credentials."
+msgstr "无法使用提供的认证信息登录。"
+
+#: authtoken/serializers.py:26
+msgid "Must include \"username\" and \"password\"."
+msgstr "必须包含 “用户名” 和 “密码”。"
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
new file mode 100644
index 00000000..bf3611aa
--- /dev/null
+++ b/rest_framework/metadata.py
@@ -0,0 +1,138 @@
+"""
+The metadata API is used to allow customization of how `OPTIONS` requests
+are handled. We currently provide a single default implementation that returns
+some fairly ad-hoc information about the view.
+
+Future implementations might use JSON schema or other definitions in order
+to return this information in a more standardized way.
+"""
+from __future__ import unicode_literals
+
+from django.core.exceptions import PermissionDenied
+from django.http import Http404
+from django.utils.encoding import force_text
+from rest_framework import exceptions, serializers
+from rest_framework.compat import OrderedDict
+from rest_framework.request import clone_request
+from rest_framework.utils.field_mapping import ClassLookupDict
+
+
+class BaseMetadata(object):
+ def determine_metadata(self, request, view):
+ """
+ Return a dictionary of metadata about the view.
+ Used to return responses for OPTIONS requests.
+ """
+ raise NotImplementedError(".determine_metadata() must be overridden.")
+
+
+class SimpleMetadata(BaseMetadata):
+ """
+ This is the default metadata implementation.
+ It returns an ad-hoc set of information about the view.
+ There are not any formalized standards for `OPTIONS` responses
+ for us to base this on.
+ """
+ label_lookup = ClassLookupDict({
+ serializers.Field: 'field',
+ serializers.BooleanField: 'boolean',
+ serializers.CharField: 'string',
+ serializers.URLField: 'url',
+ serializers.EmailField: 'email',
+ serializers.RegexField: 'regex',
+ serializers.SlugField: 'slug',
+ serializers.IntegerField: 'integer',
+ serializers.FloatField: 'float',
+ serializers.DecimalField: 'decimal',
+ serializers.DateField: 'date',
+ serializers.DateTimeField: 'datetime',
+ serializers.TimeField: 'time',
+ serializers.ChoiceField: 'choice',
+ serializers.MultipleChoiceField: 'multiple choice',
+ serializers.FileField: 'file upload',
+ serializers.ImageField: 'image upload',
+ })
+
+ def determine_metadata(self, request, view):
+ metadata = OrderedDict()
+ metadata['name'] = view.get_view_name()
+ metadata['description'] = view.get_view_description()
+ metadata['renders'] = [renderer.media_type for renderer in view.renderer_classes]
+ metadata['parses'] = [parser.media_type for parser in view.parser_classes]
+ if hasattr(view, 'get_serializer'):
+ actions = self.determine_actions(request, view)
+ if actions:
+ metadata['actions'] = actions
+ return metadata
+
+ def determine_actions(self, request, view):
+ """
+ For generic class based views we return information about
+ the fields that are accepted for 'PUT' and 'POST' methods.
+ """
+ actions = {}
+ for method in set(['PUT', 'POST']) & set(view.allowed_methods):
+ view.request = clone_request(request, method)
+ try:
+ # Test global permissions
+ if hasattr(view, 'check_permissions'):
+ view.check_permissions(view.request)
+ # Test object permissions
+ if method == 'PUT' and hasattr(view, 'get_object'):
+ view.get_object()
+ except (exceptions.APIException, PermissionDenied, Http404):
+ pass
+ else:
+ # If user has appropriate permissions for the view, include
+ # appropriate metadata about the fields that should be supplied.
+ serializer = view.get_serializer()
+ actions[method] = self.get_serializer_info(serializer)
+ finally:
+ view.request = request
+
+ return actions
+
+ def get_serializer_info(self, serializer):
+ """
+ Given an instance of a serializer, return a dictionary of metadata
+ about its fields.
+ """
+ if hasattr(serializer, 'child'):
+ # If this is a `ListSerializer` then we want to examine the
+ # underlying child serializer instance instead.
+ serializer = serializer.child
+ return OrderedDict([
+ (field_name, self.get_field_info(field))
+ for field_name, field in serializer.fields.items()
+ ])
+
+ def get_field_info(self, field):
+ """
+ Given an instance of a serializer field, return a dictionary
+ of metadata about it.
+ """
+ field_info = OrderedDict()
+ field_info['type'] = self.label_lookup[field]
+ field_info['required'] = getattr(field, 'required', False)
+
+ attrs = [
+ 'read_only', 'label', 'help_text',
+ 'min_length', 'max_length',
+ 'min_value', 'max_value'
+ ]
+
+ for attr in attrs:
+ value = getattr(field, attr, None)
+ if value is not None and value != '':
+ field_info[attr] = force_text(value, strings_only=True)
+
+ if hasattr(field, 'choices'):
+ field_info['choices'] = [
+ {
+ 'value': choice_value,
+ 'display_name': force_text(choice_name, strings_only=True)
+ }
+ for choice_value, choice_name in field.choices.items()
+ ]
+
+ return field_info
diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py
index f11def6d..c34cfcee 100644
--- a/rest_framework/mixins.py
+++ b/rest_framework/mixins.py
@@ -5,39 +5,9 @@ We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways.
"""
from __future__ import unicode_literals
-
-from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
-from rest_framework.request import clone_request
-import warnings
-
-
-def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
- """
- Given a model instance, and an optional pk and slug field,
- return the full list of all other field names on that model.
-
- For use when performing full_clean on a model instance,
- so we only clean the required fields.
- """
- include = []
-
- if pk:
- # Pending deprecation
- pk_field = obj._meta.pk
- while pk_field.rel:
- pk_field = pk_field.rel.to._meta.pk
- include.append(pk_field.name)
-
- if slug_field:
- # Pending deprecation
- include.append(slug_field)
-
- if lookup_field and lookup_field != 'pk':
- include.append(lookup_field)
-
- return [field.name for field in obj._meta.fields if field.name not in include]
+from rest_framework.settings import api_settings
class CreateModelMixin(object):
@@ -45,21 +15,18 @@ class CreateModelMixin(object):
Create a model instance.
"""
def create(self, request, *args, **kwargs):
- serializer = self.get_serializer(data=request.DATA, files=request.FILES)
+ serializer = self.get_serializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ self.perform_create(serializer)
+ headers = self.get_success_headers(serializer.data)
+ return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
- if serializer.is_valid():
- self.pre_save(serializer.object)
- self.object = serializer.save(force_insert=True)
- self.post_save(self.object, created=True)
- headers = self.get_success_headers(serializer.data)
- return Response(serializer.data, status=status.HTTP_201_CREATED,
- headers=headers)
-
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ def perform_create(self, serializer):
+ serializer.save()
def get_success_headers(self, data):
try:
- return {'Location': data['url']}
+ return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
@@ -68,31 +35,15 @@ class ListModelMixin(object):
"""
List a queryset.
"""
- empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
-
def list(self, request, *args, **kwargs):
- self.object_list = self.filter_queryset(self.get_queryset())
-
- # Default is to allow empty querysets. This can be altered by setting
- # `.allow_empty = False`, to raise 404 errors on empty querysets.
- if not self.allow_empty and not self.object_list:
- warnings.warn(
- 'The `allow_empty` parameter is due to be deprecated. '
- 'To use `allow_empty=False` style behavior, You should override '
- '`get_queryset()` and explicitly raise a 404 on empty querysets.',
- PendingDeprecationWarning
- )
- class_name = self.__class__.__name__
- error_msg = self.empty_error % {'class_name': class_name}
- raise Http404(error_msg)
-
- # Switch between paginated or standard style responses
- page = self.paginate_queryset(self.object_list)
+ queryset = self.filter_queryset(self.get_queryset())
+
+ page = self.paginate_queryset(queryset)
if page is not None:
- serializer = self.get_pagination_serializer(page)
- else:
- serializer = self.get_serializer(self.object_list, many=True)
+ serializer = self.get_serializer(page, many=True)
+ return self.get_paginated_response(serializer.data)
+ serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
@@ -101,8 +52,8 @@ class RetrieveModelMixin(object):
Retrieve a model instance.
"""
def retrieve(self, request, *args, **kwargs):
- self.object = self.get_object()
- serializer = self.get_serializer(self.object)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance)
return Response(serializer.data)
@@ -112,73 +63,28 @@ class UpdateModelMixin(object):
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
- self.object = self.get_object_or_none()
-
- if self.object is None:
- created = True
- save_kwargs = {'force_insert': True}
- success_status_code = status.HTTP_201_CREATED
- else:
- created = False
- save_kwargs = {'force_update': True}
- success_status_code = status.HTTP_200_OK
-
- serializer = self.get_serializer(self.object, data=request.DATA,
- files=request.FILES, partial=partial)
-
- if serializer.is_valid():
- self.pre_save(serializer.object)
- self.object = serializer.save(**save_kwargs)
- self.post_save(self.object, created=created)
- return Response(serializer.data, status=success_status_code)
+ instance = self.get_object()
+ serializer = self.get_serializer(instance, data=request.data, partial=partial)
+ serializer.is_valid(raise_exception=True)
+ self.perform_update(serializer)
+ return Response(serializer.data)
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ def perform_update(self, serializer):
+ serializer.save()
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
- def get_object_or_none(self):
- try:
- return self.get_object()
- except Http404:
- # If this is a PUT-as-create operation, we need to ensure that
- # we have relevant permissions, as if this was a POST request.
- # This will either raise a PermissionDenied exception,
- # or simply return None
- self.check_permissions(clone_request(self.request, 'POST'))
-
- def pre_save(self, obj):
- """
- Set any attributes on the object that are implicit in the request.
- """
- # pk and/or slug attributes are implicit in the URL.
- lookup = self.kwargs.get(self.lookup_field, None)
- pk = self.kwargs.get(self.pk_url_kwarg, None)
- slug = self.kwargs.get(self.slug_url_kwarg, None)
- slug_field = slug and self.slug_field or None
-
- if lookup:
- setattr(obj, self.lookup_field, lookup)
-
- if pk:
- setattr(obj, 'pk', pk)
-
- if slug:
- setattr(obj, slug_field, slug)
-
- # Ensure we clean the attributes so that we don't eg return integer
- # pk using a string representation, as provided by the url conf kwarg.
- if hasattr(obj, 'full_clean'):
- exclude = _get_validation_exclusions(obj, pk, slug_field, self.lookup_field)
- obj.full_clean(exclude)
-
class DestroyModelMixin(object):
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
- obj = self.get_object()
- obj.delete()
+ instance = self.get_object()
+ self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
+
+ def perform_destroy(self, instance):
+ instance.delete()
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 4d205c0e..1838130a 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -38,7 +38,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
"""
# Allow URL style format override. eg. "?format=json
format_query_param = self.settings.URL_FORMAT_OVERRIDE
- format = format_suffix or request.QUERY_PARAMS.get(format_query_param)
+ format = format_suffix or request.query_params.get(format_query_param)
if format:
renderers = self.filter_renderers(renderers, format)
@@ -54,8 +54,10 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted.
- if (_MediaType(renderer.media_type).precedence >
- _MediaType(media_type).precedence):
+ if (
+ _MediaType(renderer.media_type).precedence >
+ _MediaType(media_type).precedence
+ ):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
return renderer, renderer.media_type
@@ -85,5 +87,5 @@ class DefaultContentNegotiation(BaseContentNegotiation):
Allows URL style accept override. eg. "?accept=application/json"
"""
header = request.META.get('HTTP_ACCEPT', '*/*')
- header = request.QUERY_PARAMS.get(self.settings.URL_ACCEPT_OVERRIDE, header)
+ header = request.query_params.get(self.settings.URL_ACCEPT_OVERRIDE, header)
return [token.strip() for token in header.split(',')]
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d51ea929..f41a9ae1 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -1,94 +1,729 @@
+# coding: utf-8
"""
Pagination serializers determine the structure of the output that should
be used for paginated responses.
"""
from __future__ import unicode_literals
-from rest_framework import serializers
-from rest_framework.templatetags.rest_framework import replace_query_param
+from base64 import b64encode, b64decode
+from collections import namedtuple
+from django.core.paginator import InvalidPage, Paginator as DjangoPaginator
+from django.template import Context, loader
+from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.translation import ugettext as _
+from rest_framework.compat import OrderedDict
+from rest_framework.exceptions import NotFound
+from rest_framework.response import Response
+from rest_framework.settings import api_settings
+from rest_framework.utils.urls import (
+ replace_query_param, remove_query_param
+)
+import warnings
-class NextPageField(serializers.Field):
+def _positive_int(integer_string, strict=False, cutoff=None):
"""
- Field that returns a link to the next page in paginated results.
+ Cast a string to a strictly positive integer.
"""
- page_field = 'page'
+ ret = int(integer_string)
+ if ret < 0 or (ret == 0 and strict):
+ raise ValueError()
+ if cutoff:
+ ret = min(ret, cutoff)
+ return ret
- def to_native(self, value):
- if not value.has_next():
- return None
- page = value.next_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+def _divide_with_ceil(a, b):
+ """
+ Returns 'a' divded by 'b', with any remainder rounded up.
+ """
+ if a % b:
+ return (a // b) + 1
+ return a // b
-class PreviousPageField(serializers.Field):
+
+def _get_count(queryset):
"""
- Field that returns a link to the previous page in paginated results.
+ Determine an object count, supporting either querysets or regular lists.
"""
- page_field = 'page'
+ try:
+ return queryset.count()
+ except (AttributeError, TypeError):
+ return len(queryset)
- def to_native(self, value):
- if not value.has_previous():
- return None
- page = value.previous_page_number()
- request = self.context.get('request')
- url = request and request.build_absolute_uri() or ''
- return replace_query_param(url, self.page_field, page)
+def _get_displayed_page_numbers(current, final):
+ """
+ This utility function determines a list of page numbers to display.
+ This gives us a nice contextually relevant set of page numbers.
+
+ For example:
+ current=14, final=16 -> [1, None, 13, 14, 15, 16]
+
+ This implementation gives one page to each side of the cursor,
+ or two pages to the side when the cursor is at the edge, then
+ ensures that any breaks between non-continous page numbers never
+ remove only a single page.
+
+ For an alernativative implementation which gives two pages to each side of
+ the cursor, eg. as in GitHub issue list pagination, see:
-class DefaultObjectSerializer(serializers.Field):
+ https://gist.github.com/tomchristie/321140cebb1c4a558b15
"""
- If no object serializer is specified, then this serializer will be applied
- as the default.
+ assert current >= 1
+ assert final >= current
+
+ if final <= 5:
+ return list(range(1, final + 1))
+
+ # We always include the first two pages, last two pages, and
+ # two pages either side of the current page.
+ included = set((
+ 1,
+ current - 1, current, current + 1,
+ final
+ ))
+
+ # If the break would only exclude a single page number then we
+ # may as well include the page number instead of the break.
+ if current <= 4:
+ included.add(2)
+ included.add(3)
+ if current >= final - 3:
+ included.add(final - 1)
+ included.add(final - 2)
+
+ # Now sort the page numbers and drop anything outside the limits.
+ included = [
+ idx for idx in sorted(list(included))
+ if idx > 0 and idx <= final
+ ]
+
+ # Finally insert any `...` breaks
+ if current > 4:
+ included.insert(1, None)
+ if current < final - 3:
+ included.insert(len(included) - 1, None)
+ return included
+
+
+def _get_page_links(page_numbers, current, url_func):
"""
+ Given a list of page numbers and `None` page breaks,
+ return a list of `PageLink` objects.
+ """
+ page_links = []
+ for page_number in page_numbers:
+ if page_number is None:
+ page_link = PAGE_BREAK
+ else:
+ page_link = PageLink(
+ url=url_func(page_number),
+ number=page_number,
+ is_active=(page_number == current),
+ is_break=False
+ )
+ page_links.append(page_link)
+ return page_links
- def __init__(self, source=None, context=None):
- # Note: Swallow context kwarg - only required for eg. ModelSerializer.
- super(DefaultObjectSerializer, self).__init__(source=source)
+
+def _decode_cursor(encoded):
+ """
+ Given a string representing an encoded cursor, return a `Cursor` instance.
+ """
+
+ # The offset in the cursor is used in situations where we have a
+ # nearly-unique index. (Eg millisecond precision creation timestamps)
+ # We guard against malicious users attempting to cause expensive database
+ # queries, by having a hard cap on the maximum possible size of the offset.
+ OFFSET_CUTOFF = 1000
+
+ try:
+ querystring = b64decode(encoded.encode('ascii')).decode('ascii')
+ tokens = urlparse.parse_qs(querystring, keep_blank_values=True)
+
+ offset = tokens.get('o', ['0'])[0]
+ offset = _positive_int(offset, cutoff=OFFSET_CUTOFF)
+
+ reverse = tokens.get('r', ['0'])[0]
+ reverse = bool(int(reverse))
+
+ position = tokens.get('p', [None])[0]
+ except (TypeError, ValueError):
+ return None
+
+ return Cursor(offset=offset, reverse=reverse, position=position)
-class PaginationSerializerOptions(serializers.SerializerOptions):
+def _encode_cursor(cursor):
"""
- An object that stores the options that may be provided to a
- pagination serializer by using the inner `Meta` class.
+ Given a Cursor instance, return an encoded string representation.
+ """
+ tokens = {}
+ if cursor.offset != 0:
+ tokens['o'] = str(cursor.offset)
+ if cursor.reverse:
+ tokens['r'] = '1'
+ if cursor.position is not None:
+ tokens['p'] = cursor.position
+
+ querystring = urlparse.urlencode(tokens, doseq=True)
+ return b64encode(querystring.encode('ascii')).decode('ascii')
- Accessible on the instance as `serializer.opts`.
+
+def _reverse_ordering(ordering_tuple):
+ """
+ Given an order_by tuple such as `('-created', 'uuid')` reverse the
+ ordering and return a new tuple, eg. `('created', '-uuid')`.
"""
- def __init__(self, meta):
- super(PaginationSerializerOptions, self).__init__(meta)
- self.object_serializer_class = getattr(meta, 'object_serializer_class',
- DefaultObjectSerializer)
+ def invert(x):
+ return x[1:] if (x.startswith('-')) else '-' + x
+
+ return tuple([invert(item) for item in ordering_tuple])
+
+
+Cursor = namedtuple('Cursor', ['offset', 'reverse', 'position'])
+PageLink = namedtuple('PageLink', ['url', 'number', 'is_active', 'is_break'])
+
+PAGE_BREAK = PageLink(url=None, number=None, is_active=False, is_break=True)
-class BasePaginationSerializer(serializers.Serializer):
+class BasePagination(object):
+ display_page_controls = False
+
+ def paginate_queryset(self, queryset, request, view=None): # pragma: no cover
+ raise NotImplementedError('paginate_queryset() must be implemented.')
+
+ def get_paginated_response(self, data): # pragma: no cover
+ raise NotImplementedError('get_paginated_response() must be implemented.')
+
+ def to_html(self): # pragma: no cover
+ raise NotImplementedError('to_html() must be implemented to display page controls.')
+
+
+class PageNumberPagination(BasePagination):
"""
- A base class for pagination serializers to inherit from,
- to make implementing custom serializers more easy.
+ A simple page number based style that supports page numbers as
+ query parameters. For example:
+
+ http://api.example.org/accounts/?page=4
+ http://api.example.org/accounts/?page=4&page_size=100
"""
- _options_class = PaginationSerializerOptions
- results_field = 'results'
+ # The default page size.
+ # Defaults to `None`, meaning pagination is disabled.
+ page_size = api_settings.PAGE_SIZE
+
+ # Client can control the page using this query parameter.
+ page_query_param = 'page'
+
+ # Client can control the page size using this query parameter.
+ # Default is 'None'. Set to eg 'page_size' to enable usage.
+ page_size_query_param = None
+
+ # Set to an integer to limit the maximum page size the client may request.
+ # Only relevant if 'page_size_query_param' has also been set.
+ max_page_size = None
- def __init__(self, *args, **kwargs):
+ last_page_strings = ('last',)
+
+ template = 'rest_framework/pagination/numbers.html'
+
+ invalid_page_message = _('Invalid page "{page_number}": {message}.')
+
+ def _handle_backwards_compat(self, view):
"""
- Override init to add in the object serializer field on-the-fly.
+ Prior to version 3.1, pagination was handled in the view, and the
+ attributes were set there. The attributes should now be set on
+ the pagination class, but the old style is still pending deprecation.
"""
- super(BasePaginationSerializer, self).__init__(*args, **kwargs)
- results_field = self.results_field
- object_serializer = self.opts.object_serializer_class
+ assert not (
+ getattr(view, 'pagination_serializer_class', None) or
+ getattr(api_settings, 'DEFAULT_PAGINATION_SERIALIZER_CLASS', None)
+ ), (
+ "The pagination_serializer_class attribute and "
+ "DEFAULT_PAGINATION_SERIALIZER_CLASS setting have been removed as "
+ "part of the 3.1 pagination API improvement. See the pagination "
+ "documentation for details on the new API."
+ )
- if 'context' in kwargs:
- context_kwarg = {'context': kwargs['context']}
- else:
- context_kwarg = {}
+ for (settings_key, attr_name) in (
+ ('PAGINATE_BY', 'page_size'),
+ ('PAGINATE_BY_PARAM', 'page_size_query_param'),
+ ('MAX_PAGINATE_BY', 'max_page_size')
+ ):
+ value = getattr(api_settings, settings_key, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` settings key is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ settings_key, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
+
+ for (view_attr, attr_name) in (
+ ('paginate_by', 'page_size'),
+ ('page_query_param', 'page_query_param'),
+ ('paginate_by_param', 'page_size_query_param'),
+ ('max_paginate_by', 'max_page_size')
+ ):
+ value = getattr(view, view_attr, None)
+ if value is not None:
+ setattr(self, attr_name, value)
+ warnings.warn(
+ "The `%s` view attribute is pending deprecation. "
+ "Use the `%s` attribute on the pagination class instead." % (
+ view_attr, attr_name
+ ),
+ PendingDeprecationWarning,
+ )
+
+ def paginate_queryset(self, queryset, request, view=None):
+ """
+ Paginate a queryset if required, either returning a
+ page object, or `None` if pagination is not configured for this view.
+ """
+ self._handle_backwards_compat(view)
+
+ page_size = self.get_page_size(request)
+ if not page_size:
+ return None
+
+ paginator = DjangoPaginator(queryset, page_size)
+ page_number = request.query_params.get(self.page_query_param, 1)
+ if page_number in self.last_page_strings:
+ page_number = paginator.num_pages
+
+ try:
+ self.page = paginator.page(page_number)
+ except InvalidPage as exc:
+ msg = self.invalid_page_message.format(
+ page_number=page_number, message=six.text_type(exc)
+ )
+ raise NotFound(msg)
+
+ if paginator.count > 1 and self.template is not None:
+ # The browsable API should display pagination controls.
+ self.display_page_controls = True
+
+ self.request = request
+ return list(self.page)
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('count', self.page.paginator.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_page_size(self, request):
+ if self.page_size_query_param:
+ try:
+ return _positive_int(
+ request.query_params[self.page_size_query_param],
+ strict=True,
+ cutoff=self.max_page_size
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.page_size
+
+ def get_next_link(self):
+ if not self.page.has_next():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.next_page_number()
+ return replace_query_param(url, self.page_query_param, page_number)
+
+ def get_previous_link(self):
+ if not self.page.has_previous():
+ return None
+ url = self.request.build_absolute_uri()
+ page_number = self.page.previous_page_number()
+ if page_number == 1:
+ return remove_query_param(url, self.page_query_param)
+ return replace_query_param(url, self.page_query_param, page_number)
+
+ def get_html_context(self):
+ base_url = self.request.build_absolute_uri()
+
+ def page_number_to_url(page_number):
+ if page_number == 1:
+ return remove_query_param(base_url, self.page_query_param)
+ else:
+ return replace_query_param(base_url, self.page_query_param, page_number)
+
+ current = self.page.number
+ final = self.page.paginator.num_pages
+ page_numbers = _get_displayed_page_numbers(current, final)
+ page_links = _get_page_links(page_numbers, current, page_number_to_url)
+
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ }
- self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
-class PaginationSerializer(BasePaginationSerializer):
+class LimitOffsetPagination(BasePagination):
"""
- A default implementation of a pagination serializer.
+ A limit/offset based style. For example:
+
+ http://api.example.org/accounts/?limit=100
+ http://api.example.org/accounts/?offset=400&limit=100
+ """
+ default_limit = api_settings.PAGE_SIZE
+ limit_query_param = 'limit'
+ offset_query_param = 'offset'
+ max_limit = None
+ template = 'rest_framework/pagination/numbers.html'
+
+ def paginate_queryset(self, queryset, request, view=None):
+ self.limit = self.get_limit(request)
+ self.offset = self.get_offset(request)
+ self.count = _get_count(queryset)
+ self.request = request
+ if self.count > self.limit and self.template is not None:
+ self.display_page_controls = True
+ return list(queryset[self.offset:self.offset + self.limit])
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('count', self.count),
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_limit(self, request):
+ if self.limit_query_param:
+ try:
+ return _positive_int(
+ request.query_params[self.limit_query_param],
+ cutoff=self.max_limit
+ )
+ except (KeyError, ValueError):
+ pass
+
+ return self.default_limit
+
+ def get_offset(self, request):
+ try:
+ return _positive_int(
+ request.query_params[self.offset_query_param],
+ )
+ except (KeyError, ValueError):
+ return 0
+
+ def get_next_link(self):
+ if self.offset + self.limit >= self.count:
+ return None
+
+ url = self.request.build_absolute_uri()
+ offset = self.offset + self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
+
+ def get_previous_link(self):
+ if self.offset <= 0:
+ return None
+
+ url = self.request.build_absolute_uri()
+
+ if self.offset - self.limit <= 0:
+ return remove_query_param(url, self.offset_query_param)
+
+ offset = self.offset - self.limit
+ return replace_query_param(url, self.offset_query_param, offset)
+
+ def get_html_context(self):
+ base_url = self.request.build_absolute_uri()
+ current = _divide_with_ceil(self.offset, self.limit) + 1
+ # The number of pages is a little bit fiddly.
+ # We need to sum both the number of pages from current offset to end
+ # plus the number of pages up to the current offset.
+ # When offset is not strictly divisible by the limit then we may
+ # end up introducing an extra page as an artifact.
+ final = (
+ _divide_with_ceil(self.count - self.offset, self.limit) +
+ _divide_with_ceil(self.offset, self.limit)
+ )
+
+ def page_number_to_url(page_number):
+ if page_number == 1:
+ return remove_query_param(base_url, self.offset_query_param)
+ else:
+ offset = self.offset + ((page_number - current) * self.limit)
+ return replace_query_param(base_url, self.offset_query_param, offset)
+
+ page_numbers = _get_displayed_page_numbers(current, final)
+ page_links = _get_page_links(page_numbers, current, page_number_to_url)
+
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link(),
+ 'page_links': page_links
+ }
+
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
+
+
+class CursorPagination(BasePagination):
"""
- count = serializers.Field(source='paginator.count')
- next = NextPageField(source='*')
- previous = PreviousPageField(source='*')
+ The cursor pagination implementation is neccessarily complex.
+ For an overview of the position/offset style we use, see this post:
+ http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/
+ """
+ cursor_query_param = 'cursor'
+ page_size = api_settings.PAGE_SIZE
+ invalid_cursor_message = _('Invalid cursor')
+ ordering = '-created'
+ template = 'rest_framework/pagination/previous_and_next.html'
+
+ def paginate_queryset(self, queryset, request, view=None):
+ self.base_url = request.build_absolute_uri()
+ self.ordering = self.get_ordering(request, queryset, view)
+
+ # Determine if we have a cursor, and if so then decode it.
+ encoded = request.query_params.get(self.cursor_query_param)
+ if encoded is None:
+ self.cursor = None
+ (offset, reverse, current_position) = (0, False, None)
+ else:
+ self.cursor = _decode_cursor(encoded)
+ if self.cursor is None:
+ raise NotFound(self.invalid_cursor_message)
+ (offset, reverse, current_position) = self.cursor
+
+ # Cursor pagination always enforces an ordering.
+ if reverse:
+ queryset = queryset.order_by(*_reverse_ordering(self.ordering))
+ else:
+ queryset = queryset.order_by(*self.ordering)
+
+ # If we have a cursor with a fixed position then filter by that.
+ if current_position is not None:
+ order = self.ordering[0]
+ is_reversed = order.startswith('-')
+ order_attr = order.lstrip('-')
+
+ # Test for: (cursor reversed) XOR (queryset reversed)
+ if self.cursor.reverse != is_reversed:
+ kwargs = {order_attr + '__lt': current_position}
+ else:
+ kwargs = {order_attr + '__gt': current_position}
+
+ queryset = queryset.filter(**kwargs)
+
+ # If we have an offset cursor then offset the entire page by that amount.
+ # We also always fetch an extra item in order to determine if there is a
+ # page following on from this one.
+ results = list(queryset[offset:offset + self.page_size + 1])
+ self.page = list(results[:self.page_size])
+
+ # Determine the position of the final item following the page.
+ if len(results) > len(self.page):
+ has_following_postion = True
+ following_position = self._get_position_from_instance(results[-1], self.ordering)
+ else:
+ has_following_postion = False
+ following_position = None
+
+ # If we have a reverse queryset, then the query ordering was in reverse
+ # so we need to reverse the items again before returning them to the user.
+ if reverse:
+ self.page = list(reversed(self.page))
+
+ if reverse:
+ # Determine next and previous positions for reverse cursors.
+ self.has_next = (current_position is not None) or (offset > 0)
+ self.has_previous = has_following_postion
+ if self.has_next:
+ self.next_position = current_position
+ if self.has_previous:
+ self.previous_position = following_position
+ else:
+ # Determine next and previous positions for forward cursors.
+ self.has_next = has_following_postion
+ self.has_previous = (current_position is not None) or (offset > 0)
+ if self.has_next:
+ self.next_position = following_position
+ if self.has_previous:
+ self.previous_position = current_position
+
+ # Display page controls in the browsable API if there is more
+ # than one page.
+ if (self.has_previous or self.has_next) and self.template is not None:
+ self.display_page_controls = True
+
+ return self.page
+
+ def get_next_link(self):
+ if not self.has_next:
+ return None
+
+ if self.cursor and self.cursor.reverse and self.cursor.offset != 0:
+ # If we're reversing direction and we have an offset cursor
+ # then we cannot use the first position we find as a marker.
+ compare = self._get_position_from_instance(self.page[-1], self.ordering)
+ else:
+ compare = self.next_position
+ offset = 0
+
+ for item in reversed(self.page):
+ position = self._get_position_from_instance(item, self.ordering)
+ if position != compare:
+ # The item in this position and the item following it
+ # have different positions. We can use this position as
+ # our marker.
+ break
+
+ # The item in this postion has the same position as the item
+ # following it, we can't use it as a marker position, so increment
+ # the offset and keep seeking to the previous item.
+ compare = position
+ offset += 1
+
+ else:
+ # There were no unique positions in the page.
+ if not self.has_previous:
+ # We are on the first page.
+ # Our cursor will have an offset equal to the page size,
+ # but no position to filter against yet.
+ offset = self.page_size
+ position = None
+ elif self.cursor.reverse:
+ # The change in direction will introduce a paging artifact,
+ # where we end up skipping forward a few extra items.
+ offset = 0
+ position = self.previous_position
+ else:
+ # Use the position from the existing cursor and increment
+ # it's offset by the page size.
+ offset = self.cursor.offset + self.page_size
+ position = self.previous_position
+
+ cursor = Cursor(offset=offset, reverse=False, position=position)
+ encoded = _encode_cursor(cursor)
+ return replace_query_param(self.base_url, self.cursor_query_param, encoded)
+
+ def get_previous_link(self):
+ if not self.has_previous:
+ return None
+
+ if self.cursor and not self.cursor.reverse and self.cursor.offset != 0:
+ # If we're reversing direction and we have an offset cursor
+ # then we cannot use the first position we find as a marker.
+ compare = self._get_position_from_instance(self.page[0], self.ordering)
+ else:
+ compare = self.previous_position
+ offset = 0
+
+ for item in self.page:
+ position = self._get_position_from_instance(item, self.ordering)
+ if position != compare:
+ # The item in this position and the item following it
+ # have different positions. We can use this position as
+ # our marker.
+ break
+
+ # The item in this postion has the same position as the item
+ # following it, we can't use it as a marker position, so increment
+ # the offset and keep seeking to the previous item.
+ compare = position
+ offset += 1
+
+ else:
+ # There were no unique positions in the page.
+ if not self.has_next:
+ # We are on the final page.
+ # Our cursor will have an offset equal to the page size,
+ # but no position to filter against yet.
+ offset = self.page_size
+ position = None
+ elif self.cursor.reverse:
+ # Use the position from the existing cursor and increment
+ # it's offset by the page size.
+ offset = self.cursor.offset + self.page_size
+ position = self.next_position
+ else:
+ # The change in direction will introduce a paging artifact,
+ # where we end up skipping back a few extra items.
+ offset = 0
+ position = self.next_position
+
+ cursor = Cursor(offset=offset, reverse=True, position=position)
+ encoded = _encode_cursor(cursor)
+ return replace_query_param(self.base_url, self.cursor_query_param, encoded)
+
+ def get_ordering(self, request, queryset, view):
+ """
+ Return a tuple of strings, that may be used in an `order_by` method.
+ """
+ ordering_filters = [
+ filter_cls for filter_cls in getattr(view, 'filter_backends', [])
+ if hasattr(filter_cls, 'get_ordering')
+ ]
+
+ if ordering_filters:
+ # If a filter exists on the view that implements `get_ordering`
+ # then we defer to that filter to determine the ordering.
+ filter_cls = ordering_filters[0]
+ filter_instance = filter_cls()
+ ordering = filter_instance.get_ordering(request, queryset, view)
+ assert ordering is not None, (
+ 'Using cursor pagination, but filter class {filter_cls} '
+ 'returned a `None` ordering.'.format(
+ filter_cls=filter_cls.__name__
+ )
+ )
+ else:
+ # The default case is to check for an `ordering` attribute
+ # on this pagination instance.
+ ordering = self.ordering
+ assert ordering is not None, (
+ 'Using cursor pagination, but no ordering attribute was declared '
+ 'on the pagination class.'
+ )
+
+ assert isinstance(ordering, (six.string_types, list, tuple)), (
+ 'Invalid ordering. Expected string or tuple, but got {type}'.format(
+ type=type(ordering).__name__
+ )
+ )
+
+ if isinstance(ordering, six.string_types):
+ return (ordering,)
+ return tuple(ordering)
+
+ def _get_position_from_instance(self, instance, ordering):
+ attr = getattr(instance, ordering[0].lstrip('-'))
+ return six.text_type(attr)
+
+ def get_paginated_response(self, data):
+ return Response(OrderedDict([
+ ('next', self.get_next_link()),
+ ('previous', self.get_previous_link()),
+ ('results', data)
+ ]))
+
+ def get_html_context(self):
+ return {
+ 'previous_url': self.get_previous_link(),
+ 'next_url': self.get_next_link()
+ }
+
+ def to_html(self):
+ template = loader.get_template(self.template)
+ context = Context(self.get_html_context())
+ return template.render(context)
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index 96bfac84..437d1339 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -5,17 +5,18 @@ They give us a generic way of being able to handle various media types
on the request, such as form content or json encoded data.
"""
from __future__ import unicode_literals
+
from django.conf import settings
from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
-from rest_framework.compat import yaml, etree
+from django.utils import six
+from django.utils.six.moves.urllib import parse as urlparse
+from django.utils.encoding import force_text
from rest_framework.exceptions import ParseError
-from rest_framework.compat import six
+from rest_framework import renderers
import json
-import datetime
-import decimal
class DataAndFiles(object):
@@ -47,6 +48,7 @@ class JSONParser(BaseParser):
"""
media_type = 'application/json'
+ renderer_class = renderers.JSONRenderer
def parse(self, stream, media_type=None, parser_context=None):
"""
@@ -62,29 +64,6 @@ class JSONParser(BaseParser):
raise ParseError('JSON parse error - %s' % six.text_type(exc))
-class YAMLParser(BaseParser):
- """
- Parses YAML-serialized data.
- """
-
- media_type = 'application/yaml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as YAML and returns the resulting data.
- """
- assert yaml, 'YAMLParser requires pyyaml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
-
- try:
- data = stream.read().decode(encoding)
- return yaml.safe_load(data)
- except (ValueError, yaml.parser.ParserError) as exc:
- raise ParseError('YAML parse error - %s' % six.u(exc))
-
-
class FormParser(BaseParser):
"""
Parser for form data.
@@ -121,7 +100,8 @@ class MultiPartParser(BaseParser):
parser_context = parser_context or {}
request = parser_context['request']
encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- meta = request.META
+ meta = request.META.copy()
+ meta['CONTENT_TYPE'] = media_type
upload_handlers = request.upload_handlers
try:
@@ -129,79 +109,7 @@ class MultiPartParser(BaseParser):
data, files = parser.parse()
return DataAndFiles(data, files)
except MultiPartParserError as exc:
- raise ParseError('Multipart form parse error - %s' % six.u(exc))
-
-
-class XMLParser(BaseParser):
- """
- XML parser.
- """
-
- media_type = 'application/xml'
-
- def parse(self, stream, media_type=None, parser_context=None):
- """
- Parses the incoming bytestream as XML and returns the resulting data.
- """
- assert etree, 'XMLParser requires defusedxml to be installed'
-
- parser_context = parser_context or {}
- encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
- parser = etree.DefusedXMLParser(encoding=encoding)
- try:
- tree = etree.parse(stream, parser=parser, forbid_dtd=True)
- except (etree.ParseError, ValueError) as exc:
- raise ParseError('XML parse error - %s' % six.u(exc))
- data = self._xml_convert(tree.getroot())
-
- return data
-
- def _xml_convert(self, element):
- """
- convert the xml `element` into the corresponding python object
- """
-
- children = list(element)
-
- if len(children) == 0:
- return self._type_convert(element.text)
- else:
- # if the fist child tag is list-item means all children are list-item
- if children[0].tag == "list-item":
- data = []
- for child in children:
- data.append(self._xml_convert(child))
- else:
- data = {}
- for child in children:
- data[child.tag] = self._xml_convert(child)
-
- return data
-
- def _type_convert(self, value):
- """
- Converts the value returned by the XMl parse into the equivalent
- Python type
- """
- if value is None:
- return value
-
- try:
- return datetime.datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
- except ValueError:
- pass
-
- try:
- return int(value)
- except ValueError:
- pass
-
- try:
- return decimal.Decimal(value)
- except decimal.InvalidOperation:
- pass
-
- return value
+ raise ParseError('Multipart form parse error - %s' % six.text_type(exc))
class FileUploadParser(BaseParser):
@@ -244,7 +152,7 @@ class FileUploadParser(BaseParser):
None,
encoding)
if result is not None:
- return DataAndFiles(None, {'file': result[1]})
+ return DataAndFiles({}, {'file': result[1]})
# This is the standard case.
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
@@ -252,25 +160,26 @@ class FileUploadParser(BaseParser):
chunks = ChunkIter(stream, chunk_size)
counters = [0] * len(upload_handlers)
- for handler in upload_handlers:
+ for index, handler in enumerate(upload_handlers):
try:
handler.new_file(None, filename, content_type,
content_length, encoding)
except StopFutureHandlers:
+ upload_handlers = upload_handlers[:index + 1]
break
for chunk in chunks:
- for i, handler in enumerate(upload_handlers):
+ for index, handler in enumerate(upload_handlers):
chunk_length = len(chunk)
- chunk = handler.receive_data_chunk(chunk, counters[i])
- counters[i] += chunk_length
+ chunk = handler.receive_data_chunk(chunk, counters[index])
+ counters[index] += chunk_length
if chunk is None:
break
- for i, handler in enumerate(upload_handlers):
- file_obj = handler.file_complete(counters[i])
+ for index, handler in enumerate(upload_handlers):
+ file_obj = handler.file_complete(counters[index])
if file_obj:
- return DataAndFiles(None, {'file': file_obj})
+ return DataAndFiles({}, {'file': file_obj})
raise ParseError("FileUpload parse error - "
"none of upload handlers can handle the stream")
@@ -286,7 +195,23 @@ class FileUploadParser(BaseParser):
try:
meta = parser_context['request'].META
- disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'])
- return disposition[1]['filename']
- except (AttributeError, KeyError):
+ disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
+ filename_parm = disposition[1]
+ if 'filename*' in filename_parm:
+ return self.get_encoded_filename(filename_parm)
+ return force_text(filename_parm['filename'])
+ except (AttributeError, KeyError, ValueError):
pass
+
+ def get_encoded_filename(self, filename_parm):
+ """
+ Handle encoded filenames per RFC6266. See also:
+ http://tools.ietf.org/html/rfc2231#section-4
+ """
+ encoded_filename = force_text(filename_parm['filename*'])
+ try:
+ charset, lang, filename = encoded_filename.split('\'', 2)
+ filename = urlparse.unquote(filename)
+ except (ValueError, LookupError):
+ filename = force_text(filename_parm['filename'])
+ return filename
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 1036663e..9069d315 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -2,13 +2,11 @@
Provides a set of pluggable permission policies.
"""
from __future__ import unicode_literals
-import inspect
-import warnings
+from django.http import Http404
+from rest_framework.compat import get_model_name
SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']
-from rest_framework.compat import oauth2_provider_scope, oauth2_constants
-
class BasePermission(object):
"""
@@ -25,13 +23,6 @@ class BasePermission(object):
"""
Return `True` if permission is granted, `False` otherwise.
"""
- if len(inspect.getargspec(self.has_permission).args) == 4:
- warnings.warn(
- 'The `obj` argument in `has_permission` is deprecated. '
- 'Use `has_object_permission()` instead for object permissions.',
- DeprecationWarning, stacklevel=2
- )
- return self.has_permission(request, view, obj)
return True
@@ -52,9 +43,7 @@ class IsAuthenticated(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_authenticated():
- return True
- return False
+ return request.user and request.user.is_authenticated()
class IsAdminUser(BasePermission):
@@ -63,9 +52,7 @@ class IsAdminUser(BasePermission):
"""
def has_permission(self, request, view):
- if request.user and request.user.is_staff:
- return True
- return False
+ return request.user and request.user.is_staff
class IsAuthenticatedOrReadOnly(BasePermission):
@@ -74,11 +61,11 @@ class IsAuthenticatedOrReadOnly(BasePermission):
"""
def has_permission(self, request, view):
- if (request.method in SAFE_METHODS or
+ return (
+ request.method in SAFE_METHODS or
request.user and
- request.user.is_authenticated()):
- return True
- return False
+ request.user.is_authenticated()
+ )
class DjangoModelPermissions(BasePermission):
@@ -115,11 +102,14 @@ class DjangoModelPermissions(BasePermission):
"""
kwargs = {
'app_label': model_cls._meta.app_label,
- 'model_name': model_cls._meta.module_name
+ 'model_name': get_model_name(model_cls)
}
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, view):
+ # Note that `.model` attribute on views is deprecated, although we
+ # enforce the deprecation on the view `get_serializer_class()` and
+ # `get_queryset()` methods, rather than here.
model_cls = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
@@ -136,11 +126,11 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls)
- if (request.user and
+ return (
+ request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and
- request.user.has_perms(perms)):
- return True
- return False
+ request.user.has_perms(perms)
+ )
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
@@ -151,24 +141,60 @@ class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
authenticated_users_only = False
-class TokenHasReadWriteScope(BasePermission):
+class DjangoObjectPermissions(DjangoModelPermissions):
"""
- The request is authenticated as a user and the token used has the right scope
+ The request is authenticated using Django's object-level permissions.
+ It requires an object-permissions-enabled backend, such as Django Guardian.
+
+ It ensures that the user is authenticated, and has the appropriate
+ `add`/`change`/`delete` permissions on the object using .has_perms.
+
+ This permission can only be applied against view classes that
+ provide a `.model` or `.queryset` attribute.
"""
- def has_permission(self, request, view):
- token = request.auth
- read_only = request.method in SAFE_METHODS
+ perms_map = {
+ 'GET': [],
+ 'OPTIONS': [],
+ 'HEAD': [],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
- if not token:
- return False
+ def get_required_object_permissions(self, method, model_cls):
+ kwargs = {
+ 'app_label': model_cls._meta.app_label,
+ 'model_name': get_model_name(model_cls)
+ }
+ return [perm % kwargs for perm in self.perms_map[method]]
- if hasattr(token, 'resource'): # OAuth 1
- return read_only or not request.auth.resource.is_readonly
- elif hasattr(token, 'scope'): # OAuth 2
- required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
- return oauth2_provider_scope.check(required, request.auth.scope)
+ def has_object_permission(self, request, view, obj):
+ model_cls = getattr(view, 'model', None)
+ queryset = getattr(view, 'queryset', None)
- assert False, ('TokenHasReadWriteScope requires either the'
- '`OAuthAuthentication` or `OAuth2Authentication` authentication '
- 'class to be used.')
+ if model_cls is None and queryset is not None:
+ model_cls = queryset.model
+
+ perms = self.get_required_object_permissions(request.method, model_cls)
+ user = request.user
+
+ if not user.has_perms(perms, obj):
+ # If the user does not have permissions we need to determine if
+ # they have read permissions to see 403, or not, and simply see
+ # a 404 response.
+
+ if request.method in ('GET', 'OPTIONS', 'HEAD'):
+ # Read permissions already checked and failed, no need
+ # to make another lookup.
+ raise Http404
+
+ read_perms = self.get_required_object_permissions('GET', model_cls)
+ if not user.has_perms(read_perms, obj):
+ raise Http404
+
+ # Has read permissions.
+ return False
+
+ return True
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index edaf76d6..3a966c5b 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,358 +1,192 @@
-"""
-Serializer fields that deal with relationships.
-
-These fields allow you to specify the style that should be used to represent
-model relationships, including hyperlinks, primary keys, or slugs.
-"""
+# coding: utf-8
from __future__ import unicode_literals
-from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
-from django import forms
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.forms import widgets
-from django.forms.models import ModelChoiceIterator
+from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured
+from django.core.urlresolvers import get_script_prefix, resolve, NoReverseMatch, Resolver404
+from django.db.models.query import QuerySet
+from django.utils import six
+from django.utils.encoding import smart_text
+from django.utils.six.moves.urllib import parse as urlparse
from django.utils.translation import ugettext_lazy as _
-from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
+from rest_framework.compat import OrderedDict
+from rest_framework.fields import get_attribute, empty, Field
from rest_framework.reverse import reverse
-from rest_framework.compat import urlparse
-from rest_framework.compat import smart_text
-import warnings
-
+from rest_framework.utils import html
-##### Relational fields #####
-
-# Not actually Writable, but subclasses may need to be.
-class RelatedField(WritableField):
+class PKOnlyObject(object):
"""
- Base class for related model fields.
-
- This represents a relationship using the unicode representation of the target.
+ This is a mock object, used for when we only need the pk of the object
+ instance, but still want to return an object with a .pk attribute,
+ in order to keep the same interface as a regular model instance.
"""
- widget = widgets.Select
- many_widget = widgets.SelectMultiple
- form_field_class = forms.ChoiceField
- many_form_field_class = forms.MultipleChoiceField
-
- cache_choices = False
- empty_label = None
- read_only = True
- many = False
-
- def __init__(self, *args, **kwargs):
-
- # 'null' is to be deprecated in favor of 'required'
- if 'null' in kwargs:
- warnings.warn('The `null` keyword argument is deprecated. '
- 'Use the `required` keyword argument instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['required'] = not kwargs.pop('null')
-
- queryset = kwargs.pop('queryset', None)
- self.many = kwargs.pop('many', self.many)
- if self.many:
- self.widget = self.many_widget
- self.form_field_class = self.many_form_field_class
-
- kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
- super(RelatedField, self).__init__(*args, **kwargs)
-
- if not self.required:
- self.empty_label = BLANK_CHOICE_DASH[0][1]
-
- self.queryset = queryset
-
- def initialize(self, parent, field_name):
- super(RelatedField, self).initialize(parent, field_name)
- if self.queryset is None and not self.read_only:
- try:
- manager = getattr(self.parent.opts.model, self.source or field_name)
- if hasattr(manager, 'related'): # Forward
- self.queryset = manager.related.model._default_manager.all()
- else: # Reverse
- self.queryset = manager.field.rel.to._default_manager.all()
- except Exception:
- msg = ('Serializer related fields must include a `queryset`' +
- ' argument or set `read_only=True')
- raise Exception(msg)
-
- ### We need this stuff to make form choices work...
-
- def prepare_value(self, obj):
- return self.to_native(obj)
-
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
+ def __init__(self, pk):
+ self.pk = pk
+
+
+# We assume that 'validators' are intended for the child serializer,
+# rather than the parent serializer.
+MANY_RELATION_KWARGS = (
+ 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
+ 'label', 'help_text', 'style', 'error_messages'
+)
+
+
+class RelatedField(Field):
+ def __init__(self, **kwargs):
+ self.queryset = kwargs.pop('queryset', None)
+ assert self.queryset is not None or kwargs.get('read_only', None), (
+ 'Relational field must provide a `queryset` argument, '
+ 'or set read_only=`True`.'
+ )
+ assert not (self.queryset is not None and kwargs.get('read_only', None)), (
+ 'Relational fields should not provide a `queryset` argument, '
+ 'when setting read_only=`True`.'
+ )
+ super(RelatedField, self).__init__(**kwargs)
+
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ManyRelatedField` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return cls.many_init(*args, **kwargs)
+ return super(RelatedField, cls).__new__(cls, *args, **kwargs)
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
"""
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- def _get_queryset(self):
- return self._queryset
-
- def _set_queryset(self, queryset):
- self._queryset = queryset
- self.widget.choices = self.choices
-
- queryset = property(_get_queryset, _set_queryset)
-
- def _get_choices(self):
- # If self._choices is set, then somebody must have manually set
- # the property self.choices. In this case, just return self._choices.
- if hasattr(self, '_choices'):
- return self._choices
-
- # Otherwise, execute the QuerySet in self.queryset to determine the
- # choices dynamically. Return a fresh ModelChoiceIterator that has not been
- # consumed. Note that we're instantiating a new ModelChoiceIterator *each*
- # time _get_choices() is called (and, thus, each time self.choices is
- # accessed) so that we can ensure the QuerySet has not been consumed. This
- # construct might look complicated but it allows for lazy evaluation of
- # the queryset.
- return ModelChoiceIterator(self)
-
- def _set_choices(self, value):
- # Setting choices also sets the choices on the widget.
- # choices can be any iterable, but we call list() on it because
- # it will be consumed more than once.
- self._choices = self.widget.choices = list(value)
-
- choices = property(_get_choices, _set_choices)
-
- ### Regular serializer stuff...
-
- def field_to_native(self, obj, field_name):
- try:
- if self.source == '*':
- return self.to_native(obj)
-
- source = self.source or field_name
- value = obj
-
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
- except ObjectDoesNotExist:
- return None
-
- if value is None:
- return None
-
- if self.many:
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
- else:
- # Also support non-queryset iterables.
- # This allows us to also support plain lists of related items.
- return [self.to_native(item) for item in value]
- return self.to_native(value)
-
- def field_from_native(self, data, files, field_name, into):
- if self.read_only:
- return
-
- try:
- if self.many:
- try:
- # Form data
- value = data.getlist(field_name)
- if value == [''] or value == []:
- raise KeyError
- except AttributeError:
- # Non-form data
- value = data[field_name]
- else:
- value = data[field_name]
- except KeyError:
- if self.partial:
- return
- value = [] if self.many else None
-
- if value in (None, '') and self.required:
- raise ValidationError(self.error_messages['required'])
- elif value in (None, ''):
- into[(self.source or field_name)] = None
- elif self.many:
- into[(self.source or field_name)] = [self.from_native(item) for item in value]
- else:
- into[(self.source or field_name)] = self.from_native(value)
-
-
-### PrimaryKey relationships
-
-class PrimaryKeyRelatedField(RelatedField):
- """
- Represents a relationship as a pk value.
- """
- read_only = False
+ This method handles creating a parent `ManyRelatedField` instance
+ when the `many=True` keyword argument is passed.
- default_error_messages = {
- 'does_not_exist': _("Invalid pk '%s' - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected pk value, received %s.'),
- }
+ Typically you won't need to override this method.
- # TODO: Remove these field hacks...
- def prepare_value(self, obj):
- return self.to_native(obj.pk)
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
- def label_from_instance(self, obj):
- """
- Return a readable representation for use with eg. select widgets.
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomManyRelatedField(*args, **kwargs)
"""
- desc = smart_text(obj)
- ident = smart_text(self.to_native(obj.pk))
- if desc == ident:
- return desc
- return "%s - %s" % (desc, ident)
-
- # TODO: Possibly change this to just take `obj`, through prob less performant
- def to_native(self, pk):
- return pk
+ list_kwargs = {'child_relation': cls(*args, **kwargs)}
+ for key in kwargs.keys():
+ if key in MANY_RELATION_KWARGS:
+ list_kwargs[key] = kwargs[key]
+ return ManyRelatedField(**list_kwargs)
+
+ def run_validation(self, data=empty):
+ # We force empty strings to None values for relational fields.
+ if data == '':
+ data = None
+ return super(RelatedField, self).run_validation(data)
+
+ def get_queryset(self):
+ queryset = self.queryset
+ if isinstance(queryset, QuerySet):
+ # Ensure queryset is re-evaluated whenever used.
+ queryset = queryset.all()
+ return queryset
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
+ def use_pk_only_optimization(self):
+ return False
- try:
- return self.queryset.get(pk=data)
- except ObjectDoesNotExist:
- msg = self.error_messages['does_not_exist'] % smart_text(data)
- raise ValidationError(msg)
- except (TypeError, ValueError):
- received = type(data).__name__
- msg = self.error_messages['incorrect_type'] % received
- raise ValidationError(msg)
-
- def field_to_native(self, obj, field_name):
- if self.many:
- # To-many relationship
-
- queryset = None
- if not self.source:
- # Prefer obj.serializable_value for performance reasons
- try:
- queryset = obj.serializable_value(field_name)
- except AttributeError:
- pass
- if queryset is None:
- # RelatedManager (reverse relationship)
- source = self.source or field_name
- queryset = obj
- for component in source.split('.'):
- queryset = get_component(queryset, component)
-
- # Forward relationship
- if is_simple_callable(getattr(queryset, 'all', None)):
- return [self.to_native(item.pk) for item in queryset.all()]
- else:
- # Also support non-queryset iterables.
- # This allows us to also support plain lists of related items.
- return [self.to_native(item.pk) for item in queryset]
-
- # To-one relationship
- try:
- # Prefer obj.serializable_value for performance reasons
- pk = obj.serializable_value(self.source or field_name)
- except AttributeError:
- # RelatedObject (reverse relationship)
+ def get_attribute(self, instance):
+ if self.use_pk_only_optimization() and self.source_attrs:
+ # Optimized case, return a mock object only containing the pk attribute.
try:
- pk = getattr(obj, self.source or field_name).pk
- except ObjectDoesNotExist:
- return None
-
- # Forward relationship
- return self.to_native(pk)
+ instance = get_attribute(instance, self.source_attrs[:-1])
+ return PKOnlyObject(pk=instance.serializable_value(self.source_attrs[-1]))
+ except AttributeError:
+ pass
+ # Standard case, return the object instance.
+ return get_attribute(instance, self.source_attrs)
-### Slug relationships
+ @property
+ def choices(self):
+ return OrderedDict([
+ (
+ six.text_type(self.to_representation(item)),
+ six.text_type(item)
+ )
+ for item in self.queryset.all()
+ ])
-class SlugRelatedField(RelatedField):
+class StringRelatedField(RelatedField):
"""
- Represents a relationship using a unique field on the target.
+ A read only field that represents its targets using their
+ plain string representation.
"""
- read_only = False
- default_error_messages = {
- 'does_not_exist': _("Object with %s=%s does not exist."),
- 'invalid': _('Invalid value.'),
- }
+ def __init__(self, **kwargs):
+ kwargs['read_only'] = True
+ super(StringRelatedField, self).__init__(**kwargs)
- def __init__(self, *args, **kwargs):
- self.slug_field = kwargs.pop('slug_field', None)
- assert self.slug_field, 'slug_field is required'
- super(SlugRelatedField, self).__init__(*args, **kwargs)
+ def to_representation(self, value):
+ return six.text_type(value)
- def to_native(self, obj):
- return getattr(obj, self.slug_field)
- def from_native(self, data):
- if self.queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
+class PrimaryKeyRelatedField(RelatedField):
+ default_error_messages = {
+ 'required': _('This field is required.'),
+ 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
+ }
+
+ def use_pk_only_optimization(self):
+ return True
+ def to_internal_value(self, data):
try:
- return self.queryset.get(**{self.slug_field: data})
+ return self.get_queryset().get(pk=data)
except ObjectDoesNotExist:
- raise ValidationError(self.error_messages['does_not_exist'] %
- (self.slug_field, smart_text(data)))
+ self.fail('does_not_exist', pk_value=data)
except (TypeError, ValueError):
- msg = self.error_messages['invalid']
- raise ValidationError(msg)
+ self.fail('incorrect_type', data_type=type(data).__name__)
+ def to_representation(self, value):
+ return value.pk
-### Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
- """
- Represents a relationship using hyperlinking.
- """
- read_only = False
lookup_field = 'pk'
default_error_messages = {
- 'no_match': _('Invalid hyperlink - No URL match'),
- 'incorrect_match': _('Invalid hyperlink - Incorrect URL match'),
- 'configuration_error': _('Invalid hyperlink due to configuration error'),
- 'does_not_exist': _("Invalid hyperlink - object does not exist."),
- 'incorrect_type': _('Incorrect type. Expected url string, received %s.'),
+ 'required': _('This field is required.'),
+ 'no_match': _('Invalid hyperlink - No URL match.'),
+ 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'),
+ 'does_not_exist': _('Invalid hyperlink - Object does not exist.'),
+ 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'),
}
- # These are all pending deprecation
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
-
- def __init__(self, *args, **kwargs):
- try:
- self.view_name = kwargs.pop('view_name')
- except KeyError:
- raise ValueError("Hyperlinked field requires 'view_name' kwarg")
-
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
+ self.view_name = view_name
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
+ self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
self.format = kwargs.pop('format', None)
- # These are pending deprecation
- if 'pk_url_kwarg' in kwargs:
- msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_url_kwarg' in kwargs:
- msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_field' in kwargs:
- msg = 'slug_field is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
+ # We include this simply for dependency injection in tests.
+ # We can't add it as a class attributes or it would expect an
+ # implicit `self` argument to be passed.
+ self.reverse = reverse
- self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
- self.slug_field = kwargs.pop('slug_field', self.slug_field)
- default_slug_kwarg = self.slug_url_kwarg or self.slug_field
- self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
+ super(HyperlinkedRelatedField, self).__init__(**kwargs)
- super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
+ def use_pk_only_optimization(self):
+ return self.lookup_field == 'pk'
+
+ def get_object(self, view_name, view_args, view_kwargs):
+ """
+ Return the object corresponding to a matched URL.
+
+ Takes the matched URL conf arguments, and should return an
+ object instance, or raise an `ObjectDoesNotExist` exception.
+ """
+ lookup_value = view_kwargs[self.lookup_url_kwarg]
+ lookup_kwargs = {self.lookup_field: lookup_value}
+ return self.get_queryset().get(**lookup_kwargs)
def get_url(self, obj, view_name, request, format):
"""
@@ -361,180 +195,57 @@ class HyperlinkedRelatedField(RelatedField):
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
- lookup_field = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_field: lookup_field}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
-
- if self.pk_url_kwarg != 'pk':
- # Only try pk if it has been explicitly set.
- # Otherwise, the default `lookup_field = 'pk'` has us covered.
- pk = obj.pk
- kwargs = {self.pk_url_kwarg: pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
-
- slug = getattr(obj, self.slug_field, None)
- if slug is not None:
- # Only try slug if it corresponds to an attribute on the object.
- kwargs = {self.slug_url_kwarg: slug}
- try:
- ret = reverse(view_name, kwargs=kwargs, request=request, format=format)
- if self.slug_field == 'slug' and self.slug_url_kwarg == 'slug':
- # If the lookup succeeds using the default slug params,
- # then `slug_field` is being used implicitly, and we
- # we need to warn about the pending deprecation.
- msg = 'Implicit slug field hyperlinked fields are pending deprecation.' \
- 'You should set `lookup_field=slug` on the HyperlinkedRelatedField.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- return ret
- except NoReverseMatch:
- pass
+ # Unsaved objects will not yet have a valid URL.
+ if obj.pk is None:
+ return None
- raise NoReverseMatch()
+ lookup_value = getattr(obj, self.lookup_field)
+ kwargs = {self.lookup_url_kwarg: lookup_value}
+ return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
- def get_object(self, queryset, view_name, view_args, view_kwargs):
- """
- Return the object corresponding to a matched URL.
-
- Takes the matched URL conf arguments, and the queryset, and should
- return an object instance, or raise an `ObjectDoesNotExist` exception.
- """
- lookup = view_kwargs.get(self.lookup_field, None)
- pk = view_kwargs.get(self.pk_url_kwarg, None)
- slug = view_kwargs.get(self.slug_url_kwarg, None)
-
- if lookup is not None:
- filter_kwargs = {self.lookup_field: lookup}
- elif pk is not None:
- filter_kwargs = {'pk': pk}
- elif slug is not None:
- filter_kwargs = {self.slug_field: slug}
- else:
- raise ObjectDoesNotExist()
-
- return queryset.get(**filter_kwargs)
-
- def to_native(self, obj):
- view_name = self.view_name
+ def to_internal_value(self, data):
request = self.context.get('request', None)
- format = self.format or self.context.get('format', None)
-
- if request is None:
- msg = (
- "Using `HyperlinkedRelatedField` without including the request "
- "in the serializer context is deprecated. "
- "Add `context={'request': request}` when instantiating "
- "the serializer."
- )
- warnings.warn(msg, DeprecationWarning, stacklevel=4)
-
- # If the object has not yet been saved then we cannot hyperlink to it.
- if getattr(obj, 'pk', None) is None:
- return
-
- # Return the hyperlink, or error if incorrectly configured.
try:
- return self.get_url(obj, view_name, request, format)
- except NoReverseMatch:
- msg = (
- 'Could not resolve URL for hyperlinked relationship using '
- 'view name "%s". You may have failed to include the related '
- 'model in your API, or incorrectly configured the '
- '`lookup_field` attribute on this field.'
- )
- raise Exception(msg % view_name)
-
- def from_native(self, value):
- # Convert URL -> model instance pk
- # TODO: Use values_list
- queryset = self.queryset
- if queryset is None:
- raise Exception('Writable related fields must include a `queryset` argument')
-
- try:
- http_prefix = value.startswith(('http:', 'https:'))
+ http_prefix = data.startswith(('http:', 'https:'))
except AttributeError:
- msg = self.error_messages['incorrect_type']
- raise ValidationError(msg % type(value).__name__)
+ self.fail('incorrect_type', data_type=type(data).__name__)
if http_prefix:
# If needed convert absolute URLs to relative path
- value = urlparse.urlparse(value).path
+ data = urlparse.urlparse(data).path
prefix = get_script_prefix()
- if value.startswith(prefix):
- value = '/' + value[len(prefix):]
+ if data.startswith(prefix):
+ data = '/' + data[len(prefix):]
try:
- match = resolve(value)
- except Exception:
- raise ValidationError(self.error_messages['no_match'])
-
- if match.view_name != self.view_name:
- raise ValidationError(self.error_messages['incorrect_match'])
+ match = resolve(data)
+ except Resolver404:
+ self.fail('no_match')
try:
- return self.get_object(queryset, match.view_name,
- match.args, match.kwargs)
- except (ObjectDoesNotExist, TypeError, ValueError):
- raise ValidationError(self.error_messages['does_not_exist'])
-
-
-class HyperlinkedIdentityField(Field):
- """
- Represents the instance, or a property on the instance, using hyperlinking.
- """
- lookup_field = 'pk'
- read_only = True
+ expected_viewname = request.versioning_scheme.get_versioned_viewname(
+ self.view_name, request
+ )
+ except AttributeError:
+ expected_viewname = self.view_name
- # These are all pending deprecation
- pk_url_kwarg = 'pk'
- slug_field = 'slug'
- slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
+ if match.view_name != expected_viewname:
+ self.fail('incorrect_match')
- def __init__(self, *args, **kwargs):
try:
- self.view_name = kwargs.pop('view_name')
- except KeyError:
- msg = "HyperlinkedIdentityField requires 'view_name' argument"
- raise ValueError(msg)
+ return self.get_object(match.view_name, match.args, match.kwargs)
+ except (ObjectDoesNotExist, TypeError, ValueError):
+ self.fail('does_not_exist')
- self.format = kwargs.pop('format', None)
- lookup_field = kwargs.pop('lookup_field', None)
- self.lookup_field = lookup_field or self.lookup_field
-
- # These are pending deprecation
- if 'pk_url_kwarg' in kwargs:
- msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_url_kwarg' in kwargs:
- msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
- if 'slug_field' in kwargs:
- msg = 'slug_field is pending deprecation. Use lookup_field instead.'
- warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
-
- self.slug_field = kwargs.pop('slug_field', self.slug_field)
- default_slug_kwarg = self.slug_url_kwarg or self.slug_field
- self.pk_url_kwarg = kwargs.pop('pk_url_kwarg', self.pk_url_kwarg)
- self.slug_url_kwarg = kwargs.pop('slug_url_kwarg', default_slug_kwarg)
-
- super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
-
- def field_to_native(self, obj, field_name):
+ def to_representation(self, value):
request = self.context.get('request', None)
format = self.context.get('format', None)
- view_name = self.view_name
- if request is None:
- warnings.warn("Using `HyperlinkedIdentityField` without including the "
- "request in the serializer context is deprecated. "
- "Add `context={'request': request}` when instantiating the serializer.",
- DeprecationWarning, stacklevel=4)
+ assert request is not None, (
+ "`%s` requires the request in the serializer"
+ " context. Add `context={'request': request}` when instantiating "
+ "the serializer." % self.__class__.__name__
+ )
# By default use whatever format is given for the current context
# unless the target is a different type to the source.
@@ -550,7 +261,7 @@ class HyperlinkedIdentityField(Field):
# Return the hyperlink, or error if incorrectly configured.
try:
- return self.get_url(obj, view_name, request, format)
+ return self.get_url(value, self.view_name, request, format)
except NoReverseMatch:
msg = (
'Could not resolve URL for hyperlinked relationship using '
@@ -558,76 +269,122 @@ class HyperlinkedIdentityField(Field):
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
- raise Exception(msg % view_name)
+ raise ImproperlyConfigured(msg % self.view_name)
- def get_url(self, obj, view_name, request, format):
- """
- Given an object, return the URL that hyperlinks to the object.
- May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
- attributes are not configured to correctly match the URL conf.
- """
- lookup_field = getattr(obj, self.lookup_field)
- kwargs = {self.lookup_field: lookup_field}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+class HyperlinkedIdentityField(HyperlinkedRelatedField):
+ """
+ A read-only field that represents the identity URL for an object, itself.
- if self.pk_url_kwarg != 'pk':
- # Only try pk lookup if it has been explicitly set.
- # Otherwise, the default `lookup_field = 'pk'` has us covered.
- kwargs = {self.pk_url_kwarg: obj.pk}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+ This is in contrast to `HyperlinkedRelatedField` which represents the
+ URL of relationships to other objects.
+ """
- slug = getattr(obj, self.slug_field, None)
- if slug:
- # Only use slug lookup if a slug field exists on the model
- kwargs = {self.slug_url_kwarg: slug}
- try:
- return reverse(view_name, kwargs=kwargs, request=request, format=format)
- except NoReverseMatch:
- pass
+ def __init__(self, view_name=None, **kwargs):
+ assert view_name is not None, 'The `view_name` argument is required.'
+ kwargs['read_only'] = True
+ kwargs['source'] = '*'
+ super(HyperlinkedIdentityField, self).__init__(view_name, **kwargs)
- raise NoReverseMatch()
+ def use_pk_only_optimization(self):
+ # We have the complete object instance already. We don't need
+ # to run the 'only get the pk for this relationship' code.
+ return False
-### Old-style many classes for backwards compat
+class SlugRelatedField(RelatedField):
+ """
+ A read-write field the represents the target of the relationship
+ by a unique 'slug' attribute.
+ """
-class ManyRelatedField(RelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyRelatedField()` is deprecated. '
- 'Use `RelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManyRelatedField, self).__init__(*args, **kwargs)
+ default_error_messages = {
+ 'does_not_exist': _('Object with {slug_name}={value} does not exist.'),
+ 'invalid': _('Invalid value.'),
+ }
+ def __init__(self, slug_field=None, **kwargs):
+ assert slug_field is not None, 'The `slug_field` argument is required.'
+ self.slug_field = slug_field
+ super(SlugRelatedField, self).__init__(**kwargs)
-class ManyPrimaryKeyRelatedField(PrimaryKeyRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyPrimaryKeyRelatedField()` is deprecated. '
- 'Use `PrimaryKeyRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManyPrimaryKeyRelatedField, self).__init__(*args, **kwargs)
+ def to_internal_value(self, data):
+ try:
+ return self.get_queryset().get(**{self.slug_field: data})
+ except ObjectDoesNotExist:
+ self.fail('does_not_exist', slug_name=self.slug_field, value=smart_text(data))
+ except (TypeError, ValueError):
+ self.fail('invalid')
+
+ def to_representation(self, obj):
+ return getattr(obj, self.slug_field)
-class ManySlugRelatedField(SlugRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManySlugRelatedField()` is deprecated. '
- 'Use `SlugRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManySlugRelatedField, self).__init__(*args, **kwargs)
+class ManyRelatedField(Field):
+ """
+ Relationships with `many=True` transparently get coerced into instead being
+ a ManyRelatedField with a child relationship.
+ The `ManyRelatedField` class is responsible for handling iterating through
+ the values and passing each one to the child relationship.
-class ManyHyperlinkedRelatedField(HyperlinkedRelatedField):
- def __init__(self, *args, **kwargs):
- warnings.warn('`ManyHyperlinkedRelatedField()` is deprecated. '
- 'Use `HyperlinkedRelatedField(many=True)` instead.',
- DeprecationWarning, stacklevel=2)
- kwargs['many'] = True
- super(ManyHyperlinkedRelatedField, self).__init__(*args, **kwargs)
+ This class is treated as private API.
+ You shouldn't generally need to be using this class directly yourself,
+ and should instead simply set 'many=True' on the relationship.
+ """
+ initial = []
+ default_empty_html = []
+
+ def __init__(self, child_relation=None, *args, **kwargs):
+ self.child_relation = child_relation
+ assert child_relation is not None, '`child_relation` is a required argument.'
+ super(ManyRelatedField, self).__init__(*args, **kwargs)
+ self.child_relation.bind(field_name='', parent=self)
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ # Don't return [] if the update is partial
+ if self.field_name not in dictionary:
+ if getattr(self.root, 'partial', False):
+ return empty
+ return dictionary.getlist(self.field_name)
+
+ return dictionary.get(self.field_name, empty)
+
+ def to_internal_value(self, data):
+ return [
+ self.child_relation.to_internal_value(item)
+ for item in data
+ ]
+
+ def get_attribute(self, instance):
+ # Can't have any relationships if not created
+ if not instance.pk:
+ return []
+
+ relationship = get_attribute(instance, self.source_attrs)
+ return relationship.all() if (hasattr(relationship, 'all')) else relationship
+
+ def to_representation(self, iterable):
+ return [
+ self.child_relation.to_representation(value)
+ for value in iterable
+ ]
+
+ @property
+ def choices(self):
+ queryset = self.child_relation.queryset
+ iterable = queryset.all() if (hasattr(queryset, 'all')) else queryset
+ items_and_representations = [
+ (item, self.child_relation.to_representation(item))
+ for item in iterable
+ ]
+ return OrderedDict([
+ (
+ six.text_type(item_representation),
+ six.text_type(item) + ' - ' + six.text_type(item_representation)
+ )
+ for item, item_representation in items_and_representations
+ ])
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 3a03ca33..920d2bc4 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -8,24 +8,27 @@ REST framework also provides an HTML renderer the renders the browsable API.
"""
from __future__ import unicode_literals
-import copy
import json
+import django
from django import forms
from django.core.exceptions import ImproperlyConfigured
+from django.core.paginator import Page
from django.http.multipartparser import parse_header
-from django.template import RequestContext, loader, Template
+from django.template import Context, RequestContext, loader, Template
from django.test.client import encode_multipart
-from django.utils.xmlutils import SimplerXMLGenerator
-from rest_framework.compat import StringIO
-from rest_framework.compat import six
-from rest_framework.compat import smart_text
-from rest_framework.compat import yaml
+from django.utils import six
+from rest_framework import exceptions, serializers, status, VERSION
+from rest_framework.compat import SHORT_SEPARATORS, LONG_SEPARATORS, INDENT_SEPARATORS
+from rest_framework.exceptions import ParseError
from rest_framework.settings import api_settings
-from rest_framework.request import clone_request
+from rest_framework.request import is_form_media_type, override_method
from rest_framework.utils import encoders
from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.utils.formatting import get_view_name, get_view_description
-from rest_framework import exceptions, parsers, status, VERSION
+from rest_framework.utils.field_mapping import ClassLookupDict
+
+
+def zero_as_none(value):
+ return None if value == 0 else value
class BaseRenderer(object):
@@ -37,170 +40,77 @@ class BaseRenderer(object):
media_type = None
format = None
charset = 'utf-8'
+ render_style = 'text'
def render(self, data, accepted_media_type=None, renderer_context=None):
- raise NotImplemented('Renderer class requires .render() to be implemented')
+ raise NotImplementedError('Renderer class requires .render() to be implemented')
class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to JSON.
- Applies JSON's backslash-u character escaping for non-ascii characters.
"""
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
- ensure_ascii = True
- charset = 'utf-8'
- # Note that JSON encodings must be utf-8, utf-16 or utf-32.
- # See: http://www.ietf.org/rfc/rfc4627.txt
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Render `data` into JSON.
- """
- if data is None:
- return ''
+ ensure_ascii = not api_settings.UNICODE_JSON
+ compact = api_settings.COMPACT_JSON
- # If 'indent' is provided in the context, then pretty print the result.
- # E.g. If we're being called by the BrowsableAPIRenderer.
- renderer_context = renderer_context or {}
- indent = renderer_context.get('indent', None)
+ # We don't set a charset because JSON is a binary encoding,
+ # that can be encoded as utf-8, utf-16 or utf-32.
+ # See: http://www.ietf.org/rfc/rfc4627.txt
+ # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
+ charset = None
+ def get_indent(self, accepted_media_type, renderer_context):
if accepted_media_type:
# If the media type looks like 'application/json; indent=4',
# then pretty print the result.
+ # Note that we coerce `indent=0` into `indent=None`.
base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
- indent = params.get('indent', indent)
try:
- indent = max(min(int(indent), 8), 0)
- except (ValueError, TypeError):
- indent = None
-
- ret = json.dumps(data, cls=self.encoder_class,
- indent=indent, ensure_ascii=self.ensure_ascii)
-
- # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
- # but if ensure_ascii=False, the return type is underspecified,
- # and may (or may not) be unicode.
- # On python 3.x json.dumps() returns unicode strings.
- if isinstance(ret, six.text_type):
- return bytes(ret.encode(self.charset))
- return ret
-
-
-class UnicodeJSONRenderer(JSONRenderer):
- ensure_ascii = False
- charset = 'utf-8'
- """
- Renderer which serializes to JSON.
- Does *not* apply JSON's character escaping for non-ascii characters.
- """
-
-
-class JSONPRenderer(JSONRenderer):
- """
- Renderer which serializes to json,
- wrapping the json output in a callback function.
- """
-
- media_type = 'application/javascript'
- format = 'jsonp'
- callback_parameter = 'callback'
- default_callback = 'callback'
-
- def get_callback(self, renderer_context):
- """
- Determine the name of the callback to wrap around the json output.
- """
- request = renderer_context.get('request', None)
- params = request and request.QUERY_PARAMS or {}
- return params.get(self.callback_parameter, self.default_callback)
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders into jsonp, wrapping the json output in a callback function.
-
- Clients may set the callback function name using a query parameter
- on the URL, for example: ?callback=exampleCallbackName
- """
- renderer_context = renderer_context or {}
- callback = self.get_callback(renderer_context)
- json = super(JSONPRenderer, self).render(data, accepted_media_type,
- renderer_context)
- return callback.encode(self.charset) + b'(' + json + b');'
-
+ return zero_as_none(max(min(int(params['indent']), 8), 0))
+ except (KeyError, ValueError, TypeError):
+ pass
-class XMLRenderer(BaseRenderer):
- """
- Renderer which serializes to XML.
- """
-
- media_type = 'application/xml'
- format = 'xml'
- charset = 'utf-8'
+ # If 'indent' is provided in the context, then pretty print the result.
+ # E.g. If we're being called by the BrowsableAPIRenderer.
+ return renderer_context.get('indent', None)
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
- Renders *obj* into serialized XML.
+ Render `data` into JSON, returning a bytestring.
"""
if data is None:
- return ''
-
- stream = StringIO()
-
- xml = SimplerXMLGenerator(stream, self.charset)
- xml.startDocument()
- xml.startElement("root", {})
-
- self._to_xml(xml, data)
+ return bytes()
- xml.endElement("root")
- xml.endDocument()
- return stream.getvalue()
-
- def _to_xml(self, xml, data):
- if isinstance(data, (list, tuple)):
- for item in data:
- xml.startElement("list-item", {})
- self._to_xml(xml, item)
- xml.endElement("list-item")
-
- elif isinstance(data, dict):
- for key, value in six.iteritems(data):
- xml.startElement(key, {})
- self._to_xml(xml, value)
- xml.endElement(key)
-
- elif data is None:
- # Don't output any value
- pass
+ renderer_context = renderer_context or {}
+ indent = self.get_indent(accepted_media_type, renderer_context)
+ if indent is None:
+ separators = SHORT_SEPARATORS if self.compact else LONG_SEPARATORS
else:
- xml.characters(smart_text(data))
+ separators = INDENT_SEPARATORS
+ ret = json.dumps(
+ data, cls=self.encoder_class,
+ indent=indent, ensure_ascii=self.ensure_ascii,
+ separators=separators
+ )
-class YAMLRenderer(BaseRenderer):
- """
- Renderer which serializes to YAML.
- """
-
- media_type = 'application/yaml'
- format = 'yaml'
- encoder = encoders.SafeDumper
- charset = 'utf-8'
-
- def render(self, data, accepted_media_type=None, renderer_context=None):
- """
- Renders *obj* into serialized YAML.
- """
- assert yaml, 'YAMLRenderer requires pyyaml to be installed'
-
- if data is None:
- return ''
-
- return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
+ # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
+ # but if ensure_ascii=False, the return type is underspecified,
+ # and may (or may not) be unicode.
+ # On python 3.x json.dumps() returns unicode strings.
+ if isinstance(ret, six.text_type):
+ # We always fully escape \u2028 and \u2029 to ensure we output JSON
+ # that is a strict javascript subset. If bytes were returned
+ # by json.dumps() then we don't have these characters in any case.
+ # See: http://timelessrepo.com/json-isnt-a-javascript-subset
+ ret = ret.replace('\u2028', '\\u2028').replace('\u2029', '\\u2029')
+ return bytes(ret.encode('utf-8'))
+ return ret
class TemplateHTMLRenderer(BaseRenderer):
@@ -271,7 +181,11 @@ class TemplateHTMLRenderer(BaseRenderer):
return [self.template_name]
elif hasattr(view, 'get_template_names'):
return view.get_template_names()
- raise ImproperlyConfigured('Returned a template response with no template_name')
+ elif hasattr(view, 'template_name'):
+ return [view.template_name]
+ raise ImproperlyConfigured(
+ 'Returned a template response with no `template_name` attribute set on either the view or response'
+ )
def get_exception_template(self, response):
template_names = [name % {'status_code': response.status_code}
@@ -317,6 +231,132 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
return data
+class HTMLFormRenderer(BaseRenderer):
+ """
+ Renderers serializer data into an HTML form.
+
+ If the serializer was instantiated without an object then this will
+ return an HTML form not bound to any object,
+ otherwise it will return an HTML form with the appropriate initial data
+ populated from the object.
+
+ Note that rendering of field and form errors is not currently supported.
+ """
+ media_type = 'text/html'
+ format = 'form'
+ charset = 'utf-8'
+ template_pack = 'rest_framework/horizontal/'
+ base_template = 'form.html'
+
+ default_style = ClassLookupDict({
+ serializers.Field: {
+ 'base_template': 'input.html',
+ 'input_type': 'text'
+ },
+ serializers.EmailField: {
+ 'base_template': 'input.html',
+ 'input_type': 'email'
+ },
+ serializers.URLField: {
+ 'base_template': 'input.html',
+ 'input_type': 'url'
+ },
+ serializers.IntegerField: {
+ 'base_template': 'input.html',
+ 'input_type': 'number'
+ },
+ serializers.DateTimeField: {
+ 'base_template': 'input.html',
+ 'input_type': 'datetime-local'
+ },
+ serializers.DateField: {
+ 'base_template': 'input.html',
+ 'input_type': 'date'
+ },
+ serializers.TimeField: {
+ 'base_template': 'input.html',
+ 'input_type': 'time'
+ },
+ serializers.FileField: {
+ 'base_template': 'input.html',
+ 'input_type': 'file'
+ },
+ serializers.BooleanField: {
+ 'base_template': 'checkbox.html'
+ },
+ serializers.ChoiceField: {
+ 'base_template': 'select.html', # Also valid: 'radio.html'
+ },
+ serializers.MultipleChoiceField: {
+ 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
+ },
+ serializers.RelatedField: {
+ 'base_template': 'select.html', # Also valid: 'radio.html'
+ },
+ serializers.ManyRelatedField: {
+ 'base_template': 'select_multiple.html', # Also valid: 'checkbox_multiple.html'
+ },
+ serializers.Serializer: {
+ 'base_template': 'fieldset.html'
+ },
+ serializers.ListSerializer: {
+ 'base_template': 'list_fieldset.html'
+ }
+ })
+
+ def render_field(self, field, parent_style):
+ if isinstance(field._field, serializers.HiddenField):
+ return ''
+
+ style = dict(self.default_style[field])
+ style.update(field.style)
+ if 'template_pack' not in style:
+ style['template_pack'] = parent_style.get('template_pack', self.template_pack)
+ style['renderer'] = self
+
+ if style.get('input_type') == 'datetime-local' and isinstance(field.value, six.text_type):
+ field.value = field.value.rstrip('Z')
+
+ if 'template' in style:
+ template_name = style['template']
+ else:
+ template_name = style['template_pack'].strip('/') + '/' + style['base_template']
+
+ template = loader.get_template(template_name)
+ context = Context({'field': field, 'style': style})
+ return template.render(context)
+
+ def render(self, data, accepted_media_type=None, renderer_context=None):
+ """
+ Render serializer data and return an HTML form, as a string.
+ """
+ form = data.serializer
+ meta = getattr(form, 'Meta', None)
+ style = getattr(meta, 'style', {})
+ if 'template_pack' not in style:
+ style['template_pack'] = self.template_pack
+ if 'base_template' not in style:
+ style['base_template'] = self.base_template
+ style['renderer'] = self
+
+ # This API needs to be finessed and finalized for 3.1
+ if 'template' in renderer_context:
+ template_name = renderer_context['template']
+ elif 'template' in style:
+ template_name = style['template']
+ else:
+ template_name = style['template_pack'].strip('/') + '/' + style['base_template']
+
+ renderer_context = renderer_context or {}
+ request = renderer_context['request']
+ template = loader.get_template(template_name)
+ context = RequestContext(request, {
+ 'form': form,
+ 'style': style
+ })
+ return template.render(context)
+
+
class BrowsableAPIRenderer(BaseRenderer):
"""
HTML renderer used to self-document the API.
@@ -325,6 +365,7 @@ class BrowsableAPIRenderer(BaseRenderer):
format = 'api'
template = 'rest_framework/api.html'
charset = 'utf-8'
+ form_renderer_class = HTMLFormRenderer
def get_default_renderer(self, view):
"""
@@ -333,8 +374,13 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, BrowsableAPIRenderer)]
+ non_template_renderers = [renderer for renderer in renderers
+ if not hasattr(renderer, 'get_template_names')]
+
if not renderers:
return None
+ elif non_template_renderers:
+ return non_template_renderers[0]()
return renderers[0]()
def get_content(self, renderer, data,
@@ -349,7 +395,10 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
- if renderer.charset is None:
+ render_style = getattr(renderer, 'render_style', 'text')
+ assert render_style in ['text', 'binary'], 'Expected .render_style ' \
+ '"text" or "binary", but got "%s"' % render_style
+ if render_style == 'binary':
return '[%d bytes of binary content]' % len(content)
return content
@@ -358,7 +407,7 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
Returns True if a form should be shown for this method.
"""
- if not method in view.allowed_methods:
+ if method not in view.allowed_methods:
return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
@@ -372,202 +421,227 @@ class BrowsableAPIRenderer(BaseRenderer):
return False # Doesn't have permissions
return True
- def serializer_to_form_fields(self, serializer):
- fields = {}
- for k, v in serializer.get_fields().items():
- if getattr(v, 'read_only', True):
- continue
-
- kwargs = {}
- kwargs['required'] = v.required
-
- #if getattr(v, 'queryset', None):
- # kwargs['queryset'] = v.queryset
-
- if getattr(v, 'choices', None) is not None:
- kwargs['choices'] = v.choices
-
- if getattr(v, 'regex', None) is not None:
- kwargs['regex'] = v.regex
-
- if getattr(v, 'widget', None):
- widget = copy.deepcopy(v.widget)
- kwargs['widget'] = widget
-
- if getattr(v, 'default', None) is not None:
- kwargs['initial'] = v.default
-
- if getattr(v, 'label', None) is not None:
- kwargs['label'] = v.label
-
- if getattr(v, 'help_text', None) is not None:
- kwargs['help_text'] = v.help_text
-
- fields[k] = v.form_field_class(**kwargs)
-
- return fields
-
- def _get_form(self, view, method, request):
- # We need to impersonate a request with the correct method,
- # so that eg. any dynamic get_serializer_class methods return the
- # correct form for each method.
- restore = view.request
- request = clone_request(request, method)
- view.request = request
- try:
- return self.get_form(view, method, request)
- finally:
- view.request = restore
-
- def _get_raw_data_form(self, view, method, request, media_types):
- # We need to impersonate a request with the correct method,
- # so that eg. any dynamic get_serializer_class methods return the
- # correct form for each method.
- restore = view.request
- request = clone_request(request, method)
- view.request = request
- try:
- return self.get_raw_data_form(view, method, request, media_types)
- finally:
- view.request = restore
-
- def get_form(self, view, method, request):
+ def get_rendered_html_form(self, data, view, method, request):
"""
- Get a form, possibly bound to either the input or output data.
- In the absence on of the Resource having an associated form then
- provide a form that can be used to submit arbitrary content.
- """
- obj = getattr(view, 'object', None)
- if not self.show_form_for_method(view, method, request, obj):
- return
-
- if method in ('DELETE', 'OPTIONS'):
- return True # Don't actually need to return a form
+ Return a string representing a rendered HTML form, possibly bound to
+ either the input or output data.
- if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes:
- return
-
- serializer = view.get_serializer(instance=obj)
- fields = self.serializer_to_form_fields(serializer)
+ In the absence of the View having an associated form then return None.
+ """
+ # See issue #2089 for refactoring this.
+ serializer = getattr(data, 'serializer', None)
+ if serializer and not getattr(serializer, 'many', False):
+ instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
+ else:
+ instance = None
- # Creating an on the fly form see:
- # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
- OnTheFlyForm = type(str("OnTheFlyForm"), (forms.Form,), fields)
- data = (obj is not None) and serializer.data or None
- form_instance = OnTheFlyForm(data)
- return form_instance
+ # If this is valid serializer data, and the form is for the same
+ # HTTP method as was used in the request then use the existing
+ # serializer instance, rather than dynamically creating a new one.
+ if request.method == method and serializer is not None:
+ try:
+ kwargs = {'data': request.data}
+ except ParseError:
+ kwargs = {}
+ existing_serializer = serializer
+ else:
+ kwargs = {}
+ existing_serializer = None
+
+ with override_method(view, request, method) as request:
+ if not self.show_form_for_method(view, method, request, instance):
+ return
+
+ if method in ('DELETE', 'OPTIONS'):
+ return True # Don't actually need to return a form
+
+ if (
+ not getattr(view, 'get_serializer', None) or
+ not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
+ ):
+ return
+
+ if existing_serializer is not None:
+ serializer = existing_serializer
+ else:
+ if method in ('PUT', 'PATCH'):
+ serializer = view.get_serializer(instance=instance, **kwargs)
+ else:
+ serializer = view.get_serializer(**kwargs)
+
+ if hasattr(serializer, 'initial_data'):
+ serializer.is_valid()
+
+ form_renderer = self.form_renderer_class()
+ return form_renderer.render(
+ serializer.data,
+ self.accepted_media_type,
+ dict(
+ list(self.renderer_context.items()) +
+ [('template', 'rest_framework/api_form.html')]
+ )
+ )
- def get_raw_data_form(self, view, method, request, media_types):
+ def get_raw_data_form(self, data, view, method, request):
"""
Returns a form that allows for arbitrary content types to be tunneled
via standard HTML forms.
(Which are typically application/x-www-form-urlencoded)
"""
-
- # If we're not using content overloading there's no point in supplying a generic form,
- # as the view won't treat the form's value as the content of the request.
- if not (api_settings.FORM_CONTENT_OVERRIDE
- and api_settings.FORM_CONTENTTYPE_OVERRIDE):
- return None
-
- # Check permissions
- obj = getattr(view, 'object', None)
- if not self.show_form_for_method(view, method, request, obj):
- return
-
- content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
- content_field = api_settings.FORM_CONTENT_OVERRIDE
- choices = [(media_type, media_type) for media_type in media_types]
- initial = media_types[0]
-
- # NB. http://jacobian.org/writing/dynamic-form-generation/
- class GenericContentForm(forms.Form):
- def __init__(self):
- super(GenericContentForm, self).__init__()
-
- self.fields[content_type_field] = forms.ChoiceField(
- label='Media type',
- choices=choices,
- initial=initial
- )
- self.fields[content_field] = forms.CharField(
- label='Content',
- widget=forms.Textarea
- )
-
- return GenericContentForm()
+ # See issue #2089 for refactoring this.
+ serializer = getattr(data, 'serializer', None)
+ if serializer and not getattr(serializer, 'many', False):
+ instance = getattr(serializer, 'instance', None)
+ if isinstance(instance, Page):
+ instance = None
+ else:
+ instance = None
+
+ with override_method(view, request, method) as request:
+ # If we're not using content overloading there's no point in
+ # supplying a generic form, as the view won't treat the form's
+ # value as the content of the request.
+ if not (api_settings.FORM_CONTENT_OVERRIDE and
+ api_settings.FORM_CONTENTTYPE_OVERRIDE):
+ return None
+
+ # Check permissions
+ if not self.show_form_for_method(view, method, request, instance):
+ return
+
+ # If possible, serialize the initial content for the generic form
+ default_parser = view.parser_classes[0]
+ renderer_class = getattr(default_parser, 'renderer_class', None)
+ if (hasattr(view, 'get_serializer') and renderer_class):
+ # View has a serializer defined and parser class has a
+ # corresponding renderer that can be used to render the data.
+
+ if method in ('PUT', 'PATCH'):
+ serializer = view.get_serializer(instance=instance)
+ else:
+ serializer = view.get_serializer()
+
+ # Render the raw data content
+ renderer = renderer_class()
+ accepted = self.accepted_media_type
+ context = self.renderer_context.copy()
+ context['indent'] = 4
+ content = renderer.render(serializer.data, accepted, context)
+ else:
+ content = None
+
+ # Generate a generic form that includes a content type field,
+ # and a content field.
+ content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE
+ content_field = api_settings.FORM_CONTENT_OVERRIDE
+
+ media_types = [parser.media_type for parser in view.parser_classes]
+ choices = [(media_type, media_type) for media_type in media_types]
+ initial = media_types[0]
+
+ # NB. http://jacobian.org/writing/dynamic-form-generation/
+ class GenericContentForm(forms.Form):
+ def __init__(self):
+ super(GenericContentForm, self).__init__()
+
+ self.fields[content_type_field] = forms.ChoiceField(
+ label='Media type',
+ choices=choices,
+ initial=initial
+ )
+ self.fields[content_field] = forms.CharField(
+ label='Content',
+ widget=forms.Textarea,
+ initial=content
+ )
+
+ return GenericContentForm()
def get_name(self, view):
- return get_view_name(view.__class__, getattr(view, 'suffix', None))
+ return view.get_view_name()
def get_description(self, view):
- return get_view_description(view.__class__, html=True)
+ return view.get_view_description(html=True)
def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path)
- def render(self, data, accepted_media_type=None, renderer_context=None):
+ def get_context(self, data, accepted_media_type, renderer_context):
"""
- Render the HTML for the browsable API representation.
+ Returns the context used to render.
"""
- accepted_media_type = accepted_media_type or ''
- renderer_context = renderer_context or {}
-
view = renderer_context['view']
request = renderer_context['request']
response = renderer_context['response']
- media_types = [parser.media_type for parser in view.parser_classes]
renderer = self.get_default_renderer(view)
- content = self.get_content(renderer, data, accepted_media_type, renderer_context)
-
- put_form = self._get_form(view, 'PUT', request)
- post_form = self._get_form(view, 'POST', request)
- patch_form = self._get_form(view, 'PATCH', request)
- delete_form = self._get_form(view, 'DELETE', request)
- options_form = self._get_form(view, 'OPTIONS', request)
- raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
- raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
- raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
+ raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request)
+ raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request)
+ raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
- name = self.get_name(view)
- description = self.get_description(view)
- breadcrumb_list = self.get_breadcrumbs(request)
+ response_headers = dict(response.items())
+ renderer_content_type = ''
+ if renderer:
+ renderer_content_type = '%s' % renderer.media_type
+ if renderer.charset:
+ renderer_content_type += ' ;%s' % renderer.charset
+ response_headers['Content-Type'] = renderer_content_type
- template = loader.get_template(self.template)
- context = RequestContext(request, {
- 'content': content,
+ if hasattr(view, 'paginator') and view.paginator.display_page_controls:
+ paginator = view.paginator
+ else:
+ paginator = None
+
+ context = {
+ 'content': self.get_content(renderer, data, accepted_media_type, renderer_context),
'view': view,
'request': request,
'response': response,
- 'description': description,
- 'name': name,
+ 'description': self.get_description(view),
+ 'name': self.get_name(view),
'version': VERSION,
- 'breadcrumblist': breadcrumb_list,
+ 'paginator': paginator,
+ 'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
- 'available_formats': [renderer.format for renderer in view.renderer_classes],
+ 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
+ 'response_headers': response_headers,
- 'put_form': put_form,
- 'post_form': post_form,
- 'patch_form': patch_form,
- 'delete_form': delete_form,
- 'options_form': options_form,
+ 'put_form': self.get_rendered_html_form(data, view, 'PUT', request),
+ 'post_form': self.get_rendered_html_form(data, view, 'POST', request),
+ 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
+ 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form,
'raw_data_patch_form': raw_data_patch_form,
'raw_data_put_or_patch_form': raw_data_put_or_patch_form,
+ 'display_edit_forms': bool(response.status_code != 403),
+
'api_settings': api_settings
- })
+ }
+ return context
+ def render(self, data, accepted_media_type=None, renderer_context=None):
+ """
+ Render the HTML for the browsable API representation.
+ """
+ self.accepted_media_type = accepted_media_type or ''
+ self.renderer_context = renderer_context or {}
+
+ template = loader.get_template(self.template)
+ context = self.get_context(data, accepted_media_type, renderer_context)
+ context = RequestContext(renderer_context['request'], context)
ret = template.render(context)
# Munge DELETE Response code to allow us to return content
# (Do this *after* we've rendered the template so that we include
# the normal deletion response code in the output)
+ response = renderer_context['response']
if response.status_code == status.HTTP_204_NO_CONTENT:
response.status_code = status.HTTP_200_OK
@@ -578,7 +652,7 @@ class MultiPartRenderer(BaseRenderer):
media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg'
format = 'multipart'
charset = 'utf-8'
- BOUNDARY = 'BoUnDaRyStRiNg'
+ BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg'
def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data)
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 919716f4..e4b5bc26 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -4,7 +4,7 @@ The Request class is used as a wrapper around the standard request object.
The wrapped request then offers a richer API, in particular :
- content automatically parsed according to `Content-Type` header,
- and available as `request.DATA`
+ and available as `request.data`
- full support of PUT method, including support for file uploads
- form overloading of HTTP method, content type and content
"""
@@ -12,11 +12,13 @@ from __future__ import unicode_literals
from django.conf import settings
from django.http import QueryDict
from django.http.multipartparser import parse_header
+from django.utils import six
from django.utils.datastructures import MultiValueDict
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
-from rest_framework.compat import BytesIO
from rest_framework.settings import api_settings
+import sys
+import warnings
def is_form_media_type(media_type):
@@ -28,6 +30,36 @@ def is_form_media_type(media_type):
base_media_type == 'multipart/form-data')
+class override_method(object):
+ """
+ A context manager that temporarily overrides the method on a request,
+ additionally setting the `view.request` attribute.
+
+ Usage:
+
+ with override_method(view, request, 'POST') as request:
+ ... # Do stuff with `view` and `request`
+ """
+ def __init__(self, view, request, method):
+ self.view = view
+ self.request = request
+ self.method = method
+ self.action = getattr(view, 'action', None)
+
+ def __enter__(self):
+ self.view.request = clone_request(self.request, self.method)
+ if self.action is not None:
+ # For viewsets we also set the `.action` attribute.
+ action_map = getattr(self.view, 'action_map', {})
+ self.view.action = action_map.get(self.method.lower())
+ return self.view.request
+
+ def __exit__(self, *args, **kwarg):
+ self.view.request = self.request
+ if self.action is not None:
+ self.view.action = self.action
+
+
class Empty(object):
"""
Placeholder for unset attributes.
@@ -52,6 +84,7 @@ def clone_request(request, method):
parser_context=request.parser_context)
ret._data = request._data
ret._files = request._files
+ ret._full_data = request._full_data
ret._content_type = request._content_type
ret._stream = request._stream
ret._method = method
@@ -61,6 +94,14 @@ def clone_request(request, method):
ret._auth = request._auth
if hasattr(request, '_authenticator'):
ret._authenticator = request._authenticator
+ if hasattr(request, 'accepted_renderer'):
+ ret.accepted_renderer = request.accepted_renderer
+ if hasattr(request, 'accepted_media_type'):
+ ret.accepted_media_type = request.accepted_media_type
+ if hasattr(request, 'version'):
+ ret.version = request.version
+ if hasattr(request, 'versioning_scheme'):
+ ret.versioning_scheme = request.versioning_scheme
return ret
@@ -103,6 +144,7 @@ class Request(object):
self.parser_context = parser_context
self._data = Empty
self._files = Empty
+ self._full_data = Empty
self._method = Empty
self._content_type = Empty
self._stream = Empty
@@ -156,13 +198,31 @@ class Request(object):
return self._stream
@property
- def QUERY_PARAMS(self):
+ def query_params(self):
"""
More semantically correct name for request.GET.
"""
return self._request.GET
@property
+ def QUERY_PARAMS(self):
+ """
+ Synonym for `.query_params`, for backwards compatibility.
+ """
+ warnings.warn(
+ "`request.QUERY_PARAMS` is deprecated. Use `request.query_params` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
+ return self._request.GET
+
+ @property
+ def data(self):
+ if not _hasattr(self, '_full_data'):
+ self._load_data_and_files()
+ return self._full_data
+
+ @property
def DATA(self):
"""
Parses the request body and returns the data.
@@ -170,6 +230,11 @@ class Request(object):
Similar to usual behaviour of `request.POST`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
+ warnings.warn(
+ "`request.DATA` is deprecated. Use `request.data` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
if not _hasattr(self, '_data'):
self._load_data_and_files()
return self._data
@@ -182,6 +247,11 @@ class Request(object):
Similar to usual behaviour of `request.FILES`, except that it handles
arbitrary parsers, and also works on methods other than POST (eg PUT).
"""
+ warnings.warn(
+ "`request.FILES` is deprecated. Use `request.data` instead.",
+ DeprecationWarning,
+ stacklevel=1
+ )
if not _hasattr(self, '_files'):
self._load_data_and_files()
return self._files
@@ -200,10 +270,14 @@ class Request(object):
def user(self, value):
"""
Sets the user on the current request. This is necessary to maintain
- compatilbility with django.contrib.auth where the user proprety is
+ compatibility with django.contrib.auth where the user property is
set in the login and logout functions.
+
+ Note that we also set the user on Django's underlying `HttpRequest`
+ instance, ensuring that it is available to any middleware in the stack.
"""
self._user = value
+ self._request.user = value
@property
def auth(self):
@@ -222,6 +296,7 @@ class Request(object):
request, such as an authentication token.
"""
self._auth = value
+ self._request.auth = value
@property
def successful_authenticator(self):
@@ -235,13 +310,18 @@ class Request(object):
def _load_data_and_files(self):
"""
- Parses the request content into self.DATA and self.FILES.
+ Parses the request content into `self.data`.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
if not _hasattr(self, '_data'):
self._data, self._files = self._parse()
+ if self._files:
+ self._full_data = self._data.copy()
+ self._full_data.update(self._files)
+ else:
+ self._full_data = self._data
def _load_method_and_content_type(self):
"""
@@ -256,18 +336,20 @@ class Request(object):
if not _hasattr(self, '_method'):
self._method = self._request.method
- if self._method == 'POST':
- # Allow X-HTTP-METHOD-OVERRIDE header
- self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
- self._method)
+ # Allow X-HTTP-METHOD-OVERRIDE header
+ if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
+ self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
def _load_stream(self):
"""
Return the content body of the request, as a stream.
"""
try:
- content_length = int(self.META.get('CONTENT_LENGTH',
- self.META.get('HTTP_CONTENT_LENGTH')))
+ content_length = int(
+ self.META.get(
+ 'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
+ )
+ )
except (ValueError, TypeError):
content_length = 0
@@ -276,7 +358,7 @@ class Request(object):
elif hasattr(self._request, 'read'):
self._stream = self._request
else:
- self._stream = BytesIO(self.raw_post_data)
+ self._stream = six.BytesIO(self.raw_post_data)
def _perform_form_overloading(self):
"""
@@ -291,28 +373,36 @@ class Request(object):
)
# We only need to use form overloading on form POST requests.
- if (not USE_FORM_OVERLOADING
- or self._request.method != 'POST'
- or not is_form_media_type(self._content_type)):
+ if (
+ self._request.method != 'POST' or
+ not USE_FORM_OVERLOADING or
+ not is_form_media_type(self._content_type)
+ ):
return
# At this point we're committed to parsing the request as form data.
self._data = self._request.POST
self._files = self._request.FILES
+ self._full_data = self._data.copy()
+ self._full_data.update(self._files)
# Method overloading - change the method and remove the param from the content.
- if (self._METHOD_PARAM and
- self._METHOD_PARAM in self._data):
+ if (
+ self._METHOD_PARAM and
+ self._METHOD_PARAM in self._data
+ ):
self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse.
- if (self._CONTENT_PARAM and
+ if (
+ self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
- self._CONTENTTYPE_PARAM in self._data):
+ self._CONTENTTYPE_PARAM in self._data
+ ):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
- self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING))
- self._data, self._files = (Empty, Empty)
+ self._stream = six.BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
+ self._data, self._files, self._full_data = (Empty, Empty, Empty)
def _parse(self):
"""
@@ -324,7 +414,7 @@ class Request(object):
media_type = self.content_type
if stream is None or media_type is None:
- empty_data = QueryDict('', self._request._encoding)
+ empty_data = QueryDict('', encoding=self._request._encoding)
empty_files = MultiValueDict()
return (empty_data, empty_files)
@@ -333,7 +423,17 @@ class Request(object):
if not parser:
raise exceptions.UnsupportedMediaType(media_type)
- parsed = parser.parse(stream, media_type, self.parser_context)
+ try:
+ parsed = parser.parse(stream, media_type, self.parser_context)
+ except:
+ # If we get an exception during parsing, fill in empty data and
+ # re-raise. Ensures we don't simply repeat the error when
+ # attempting to render the browsable renderer response, or when
+ # logging the request or similar.
+ self._data = QueryDict('', encoding=self._request._encoding)
+ self._files = MultiValueDict()
+ self._full_data = self._data
+ raise
# Parser classes may return the raw data, or a
# DataAndFiles object. Unpack the result as required.
@@ -356,9 +456,9 @@ class Request(object):
self._not_authenticated()
raise
- if not user_auth_tuple is None:
+ if user_auth_tuple is not None:
self._authenticator = authenticator
- self._user, self._auth = user_auth_tuple
+ self.user, self.auth = user_auth_tuple
return
self._not_authenticated()
@@ -373,17 +473,25 @@ class Request(object):
self._authenticator = None
if api_settings.UNAUTHENTICATED_USER:
- self._user = api_settings.UNAUTHENTICATED_USER()
+ self.user = api_settings.UNAUTHENTICATED_USER()
else:
- self._user = None
+ self.user = None
if api_settings.UNAUTHENTICATED_TOKEN:
- self._auth = api_settings.UNAUTHENTICATED_TOKEN()
+ self.auth = api_settings.UNAUTHENTICATED_TOKEN()
else:
- self._auth = None
+ self.auth = None
- def __getattr__(self, attr):
+ def __getattribute__(self, attr):
"""
- Proxy other attributes to the underlying HttpRequest object.
+ If an attribute does not exist on this instance, then we also attempt
+ to proxy it to the underlying HttpRequest object.
"""
- return getattr(self._request, attr)
+ try:
+ return super(Request, self).__getattribute__(attr)
+ except AttributeError:
+ info = sys.exc_info()
+ try:
+ return getattr(self._request, attr)
+ except AttributeError:
+ six.reraise(info[0], info[1], info[2].tb_next)
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 5877c8a3..c21c60a2 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -7,7 +7,7 @@ The appropriate renderer is called during Django's template response rendering.
from __future__ import unicode_literals
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse
-from rest_framework.compat import six
+from django.utils import six
class Response(SimpleTemplateResponse):
@@ -16,7 +16,7 @@ class Response(SimpleTemplateResponse):
arbitrary media types.
"""
- def __init__(self, data=None, status=200,
+ def __init__(self, data=None, status=None,
template_name=None, headers=None,
exception=False, content_type=None):
"""
@@ -58,9 +58,15 @@ class Response(SimpleTemplateResponse):
ret = renderer.render(self.data, media_type, context)
if isinstance(ret, six.text_type):
- assert charset, 'renderer returned unicode, and did not specify ' \
- 'a charset value.'
+ assert charset, (
+ 'renderer returned unicode, and did not specify '
+ 'a charset value.'
+ )
return bytes(ret.encode(charset))
+
+ if not ret:
+ del self['Content-Type']
+
return ret
@property
@@ -75,10 +81,14 @@ class Response(SimpleTemplateResponse):
def __getstate__(self):
"""
- Remove attributes from the response that shouldn't be cached
+ Remove attributes from the response that shouldn't be cached.
"""
state = super(Response, self).__getstate__()
- for key in ('accepted_renderer', 'renderer_context', 'data'):
+ for key in (
+ 'accepted_renderer', 'renderer_context', 'resolver_match',
+ 'client', 'request', 'wsgi_request'
+ ):
if key in state:
del state[key]
+ state['_closable_objects'] = []
return state
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index a51b07f5..a251d99d 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -1,13 +1,26 @@
"""
-Provide reverse functions that return fully qualified URLs
+Provide urlresolver functions that return fully qualified URLs or view names
"""
from __future__ import unicode_literals
from django.core.urlresolvers import reverse as django_reverse
+from django.utils import six
from django.utils.functional import lazy
def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
+ If versioning is being used then we pass any `reverse` calls through
+ to the versioning scheme instance, so that the resulting URL
+ can be modified if needed.
+ """
+ scheme = getattr(request, 'versioning_scheme', None)
+ if scheme is not None:
+ return scheme.reverse(viewname, args, kwargs, request, format, **extra)
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+
+def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ """
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
"""
@@ -20,4 +33,4 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra
return url
-reverse_lazy = lazy(reverse, str)
+reverse_lazy = lazy(reverse, six.text_type)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 930011d3..b1e39ff7 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -17,15 +17,19 @@ from __future__ import unicode_literals
import itertools
from collections import namedtuple
+from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
-from rest_framework.compat import patterns, url
+from rest_framework.compat import get_resolver_match, OrderedDict
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
Route = namedtuple('Route', ['url', 'mapping', 'name', 'initkwargs'])
+DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwargs'])
+DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs'])
def replace_methodname(format_string, methodname):
@@ -61,13 +65,13 @@ class BaseRouter(object):
If `base_name` is not specified, attempt to automatically determine
it from the viewset.
"""
- raise NotImplemented('get_default_base_name must be overridden')
+ raise NotImplementedError('get_default_base_name must be overridden')
def get_urls(self):
"""
Return a list of URL patterns, given the registered viewsets.
"""
- raise NotImplemented('get_urls must be overridden')
+ raise NotImplementedError('get_urls must be overridden')
@property
def urls(self):
@@ -88,6 +92,14 @@ class SimpleRouter(BaseRouter):
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
+ # Dynamically generated list routes.
+ # Generated using @list_route decorator
+ # on methods of the viewset.
+ DynamicListRoute(
+ url=r'^{prefix}/{methodname}{trailing_slash}$',
+ name='{basename}-{methodnamehyphen}',
+ initkwargs={}
+ ),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
@@ -100,13 +112,10 @@ class SimpleRouter(BaseRouter):
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
- # Dynamically generated routes.
- # Generated using @action or @link decorators on methods of the viewset.
- Route(
+ # Dynamically generated detail routes.
+ # Generated using @detail_route decorator on methods of the viewset.
+ DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
- mapping={
- '{httpmethod}': '{methodname}',
- },
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
@@ -121,16 +130,13 @@ class SimpleRouter(BaseRouter):
If `base_name` is not specified, attempt to automatically determine
it from the viewset.
"""
- model_cls = getattr(viewset, 'model', None)
queryset = getattr(viewset, 'queryset', None)
- if model_cls is None and queryset is not None:
- model_cls = queryset.model
- assert model_cls, '`base_name` argument not specified, and could ' \
+ assert queryset is not None, '`base_name` argument not specified, and could ' \
'not automatically determine the name from the viewset, as ' \
- 'it does not have a `.model` or `.queryset` attribute.'
+ 'it does not have a `.queryset` attribute.'
- return model_cls._meta.object_name.lower()
+ return queryset.model._meta.object_name.lower()
def get_routes(self, viewset):
"""
@@ -139,33 +145,50 @@ class SimpleRouter(BaseRouter):
Returns a list of the Route namedtuple.
"""
- known_actions = flatten([route.mapping.values() for route in self.routes])
+ known_actions = flatten([route.mapping.values() for route in self.routes if isinstance(route, Route)])
- # Determine any `@action` or `@link` decorated methods on the viewset
- dynamic_routes = []
+ # Determine any `@detail_route` or `@list_route` decorated methods on the viewset
+ detail_routes = []
+ list_routes = []
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethods = getattr(attr, 'bind_to_methods', None)
+ detail = getattr(attr, 'detail', True)
if httpmethods:
if methodname in known_actions:
- raise ImproperlyConfigured('Cannot use @action or @link decorator on '
- 'method "%s" as it is an existing route' % methodname)
+ raise ImproperlyConfigured('Cannot use @detail_route or @list_route '
+ 'decorators on method "%s" '
+ 'as it is an existing route' % methodname)
httpmethods = [method.lower() for method in httpmethods]
- dynamic_routes.append((httpmethods, methodname))
+ if detail:
+ detail_routes.append((httpmethods, methodname))
+ else:
+ list_routes.append((httpmethods, methodname))
+
+ def _get_dynamic_routes(route, dynamic_routes):
+ ret = []
+ for httpmethods, methodname in dynamic_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ initkwargs = route.initkwargs.copy()
+ initkwargs.update(method_kwargs)
+ url_path = initkwargs.pop("url_path", None) or methodname
+ ret.append(Route(
+ url=replace_methodname(route.url, url_path),
+ mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
+ name=replace_methodname(route.name, url_path),
+ initkwargs=initkwargs,
+ ))
+
+ return ret
ret = []
for route in self.routes:
- if route.mapping == {'{httpmethod}': '{methodname}'}:
- # Dynamic routes (@link or @action decorator)
- for httpmethods, methodname in dynamic_routes:
- initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
- ret.append(Route(
- url=replace_methodname(route.url, methodname),
- mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
- initkwargs=initkwargs,
- ))
+ if isinstance(route, DynamicDetailRoute):
+ # Dynamic detail routes (@detail_route decorator)
+ ret += _get_dynamic_routes(route, detail_routes)
+ elif isinstance(route, DynamicListRoute):
+ # Dynamic list routes (@list_route decorator)
+ ret += _get_dynamic_routes(route, list_routes)
else:
# Standard route
ret.append(route)
@@ -184,14 +207,27 @@ class SimpleRouter(BaseRouter):
bound_methods[method] = action
return bound_methods
- def get_lookup_regex(self, viewset):
+ def get_lookup_regex(self, viewset, lookup_prefix=''):
"""
Given a viewset, return the portion of URL regex that is used
to match against a single instance.
+
+ Note that lookup_prefix is not used directly inside REST rest_framework
+ itself, but is required in order to nicely support nested router
+ implementations, such as drf-nested-routers.
+
+ https://github.com/alanjds/drf-nested-routers
"""
- base_regex = '(?P<{lookup_field}>[^/]+)'
+ base_regex = '(?P<{lookup_prefix}{lookup_field}>{lookup_value})'
+ # Use `pk` as default field, unset set. Default regex should not
+ # consume `.json` style suffixes and should break at '/' boundaries.
lookup_field = getattr(viewset, 'lookup_field', 'pk')
- return base_regex.format(lookup_field=lookup_field)
+ lookup_value = getattr(viewset, 'lookup_value_regex', '[^/.]+')
+ return base_regex.format(
+ lookup_prefix=lookup_prefix,
+ lookup_field=lookup_field,
+ lookup_value=lookup_value
+ )
def get_urls(self):
"""
@@ -236,7 +272,7 @@ class DefaultRouter(SimpleRouter):
"""
Return a view to use as the API root.
"""
- api_root_dict = {}
+ api_root_dict = OrderedDict()
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@@ -244,10 +280,22 @@ class DefaultRouter(SimpleRouter):
class APIRoot(views.APIView):
_ignore_model_permissions = True
- def get(self, request, format=None):
- ret = {}
+ def get(self, request, *args, **kwargs):
+ ret = OrderedDict()
+ namespace = get_resolver_match(request).namespace
for key, url_name in api_root_dict.items():
- ret[key] = reverse(url_name, request=request, format=format)
+ if namespace:
+ url_name = namespace + ':' + url_name
+ try:
+ ret[key] = reverse(
+ url_name,
+ request=request,
+ format=kwargs.get('format', None)
+ )
+ except NoReverseMatch:
+ # Don't bail out if eg. no list routes exist, only detail routes.
+ continue
+
return Response(ret)
return APIRoot.as_view()
diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py
deleted file mode 100755
index ce11b213..00000000
--- a/rest_framework/runtests/runcoverage.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#!/usr/bin/env python
-"""
-Useful tool to run the test suite for rest_framework and generate a coverage report.
-"""
-
-# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
-# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
-# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
-import os
-import sys
-
-# fix sys path so we don't need to setup PYTHONPATH
-sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
-
-from coverage import coverage
-
-
-def main():
- """Run the tests for rest_framework and generate a coverage report."""
-
- cov = coverage()
- cov.erase()
- cov.start()
-
- from django.conf import settings
- from django.test.utils import get_runner
- TestRunner = get_runner(settings)
-
- if hasattr(TestRunner, 'func_name'):
- # Pre 1.2 test runners were just functions,
- # and did not support the 'failfast' option.
- import warnings
- warnings.warn(
- 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
- DeprecationWarning
- )
- failures = TestRunner(['tests'])
- else:
- test_runner = TestRunner()
- failures = test_runner.run_tests(['tests'])
- cov.stop()
-
- # Discover the list of all modules that we should test coverage for
- import rest_framework
-
- project_dir = os.path.dirname(rest_framework.__file__)
- cov_files = []
-
- for (path, dirs, files) in os.walk(project_dir):
- # Drop tests and runtests directories from the test coverage report
- if os.path.basename(path) in ['tests', 'runtests', 'migrations']:
- continue
-
- # Drop the compat and six modules from coverage, since we're not interested in the coverage
- # of modules which are specifically for resolving environment dependant imports.
- # (Because we'll end up getting different coverage reports for it for each environment)
- if 'compat.py' in files:
- files.remove('compat.py')
-
- if 'six.py' in files:
- files.remove('six.py')
-
- # Same applies to template tags module.
- # This module has to include branching on Django versions,
- # so it's never possible for it to have full coverage.
- if 'rest_framework.py' in files:
- files.remove('rest_framework.py')
-
- cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
-
- cov.report(cov_files)
- if '--html' in sys.argv:
- cov.html_report(cov_files, directory='coverage')
- sys.exit(failures)
-
-if __name__ == '__main__':
- main()
diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py
deleted file mode 100755
index da36d23f..00000000
--- a/rest_framework/runtests/runtests.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python
-
-# http://ericholscher.com/blog/2009/jun/29/enable-setuppy-test-your-django-apps/
-# http://www.travisswicegood.com/2010/01/17/django-virtualenv-pip-and-fabric/
-# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
-import os
-import sys
-
-# fix sys path so we don't need to setup PYTHONPATH
-sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
-os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
-
-import django
-from django.conf import settings
-from django.test.utils import get_runner
-
-
-def usage():
- return """
- Usage: python runtests.py [UnitTestClass].[method]
-
- You can pass the Class name of the `UnitTestClass` you want to test.
-
- Append a method name if you only want to test a specific method of that class.
- """
-
-
-def main():
- TestRunner = get_runner(settings)
-
- test_runner = TestRunner()
- if len(sys.argv) == 2:
- test_case = '.' + sys.argv[1]
- elif len(sys.argv) == 1:
- test_case = ''
- else:
- print(usage())
- sys.exit(1)
- test_module_name = 'rest_framework.tests'
- if django.VERSION[0] == 1 and django.VERSION[1] < 6:
- test_module_name = 'tests'
-
- failures = test_runner.run_tests([test_module_name + test_case])
-
- sys.exit(failures)
-
-if __name__ == '__main__':
- main()
diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py
deleted file mode 100644
index b3702d0b..00000000
--- a/rest_framework/runtests/settings.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# Django settings for testproject project.
-
-DEBUG = True
-TEMPLATE_DEBUG = DEBUG
-DEBUG_PROPAGATE_EXCEPTIONS = True
-
-ALLOWED_HOSTS = ['*']
-
-ADMINS = (
- # ('Your Name', 'your_email@domain.com'),
-)
-
-MANAGERS = ADMINS
-
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
- 'NAME': 'sqlite.db', # Or path to database file if using sqlite3.
- 'USER': '', # Not used with sqlite3.
- 'PASSWORD': '', # Not used with sqlite3.
- 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
- 'PORT': '', # Set to empty string for default. Not used with sqlite3.
- }
-}
-
-CACHES = {
- 'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- }
-}
-
-# Local time zone for this installation. Choices can be found here:
-# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
-# although not all choices may be available on all operating systems.
-# On Unix systems, a value of None will cause Django to use the same
-# timezone as the operating system.
-# If running in a Windows environment this must be set to the same as your
-# system time zone.
-TIME_ZONE = 'Europe/London'
-
-# Language code for this installation. All choices can be found here:
-# http://www.i18nguy.com/unicode/language-identifiers.html
-LANGUAGE_CODE = 'en-uk'
-
-SITE_ID = 1
-
-# If you set this to False, Django will make some optimizations so as not
-# to load the internationalization machinery.
-USE_I18N = True
-
-# If you set this to False, Django will not format dates, numbers and
-# calendars according to the current locale
-USE_L10N = True
-
-# Absolute filesystem path to the directory that will hold user-uploaded files.
-# Example: "/home/media/media.lawrence.com/"
-MEDIA_ROOT = ''
-
-# URL that handles the media served from MEDIA_ROOT. Make sure to use a
-# trailing slash if there is a path component (optional in other cases).
-# Examples: "http://media.lawrence.com", "http://example.com/media/"
-MEDIA_URL = ''
-
-# Make this unique, and don't share it with anybody.
-SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
-
-# List of callables that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
-# 'django.template.loaders.eggs.Loader',
-)
-
-MIDDLEWARE_CLASSES = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
-)
-
-ROOT_URLCONF = 'urls'
-
-TEMPLATE_DIRS = (
- # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
- # Always use forward slashes, even on Windows.
- # Don't forget to use absolute paths, not relative paths.
-)
-
-INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.messages',
- # Uncomment the next line to enable the admin:
- # 'django.contrib.admin',
- # Uncomment the next line to enable admin documentation:
- # 'django.contrib.admindocs',
- 'rest_framework',
- 'rest_framework.authtoken',
- 'rest_framework.tests',
-)
-
-# OAuth is optional and won't work if there is no oauth_provider & oauth2
-try:
- import oauth_provider
- import oauth2
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'oauth_provider',
- )
-
-try:
- import provider
-except ImportError:
- pass
-else:
- INSTALLED_APPS += (
- 'provider',
- 'provider.oauth2',
- )
-
-STATIC_URL = '/static/'
-
-PASSWORD_HASHERS = (
- 'django.contrib.auth.hashers.SHA1PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
- 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
- 'django.contrib.auth.hashers.BCryptPasswordHasher',
- 'django.contrib.auth.hashers.MD5PasswordHasher',
- 'django.contrib.auth.hashers.CryptPasswordHasher',
-)
-
-AUTH_USER_MODEL = 'auth.User'
-
-import django
-
-if django.VERSION < (1, 3):
- INSTALLED_APPS += ('staticfiles',)
-
-
-# If we're running on the Jenkins server we want to archive the coverage reports as XML.
-import os
-if os.environ.get('HUDSON_URL', None):
- TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner'
- TEST_OUTPUT_VERBOSE = True
- TEST_OUTPUT_DESCRIPTIONS = True
- TEST_OUTPUT_DIR = 'xmlrunner'
diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py
deleted file mode 100644
index ed5baeae..00000000
--- a/rest_framework/runtests/urls.py
+++ /dev/null
@@ -1,7 +0,0 @@
-"""
-Blank URLConf just to keep runtests.py happy.
-"""
-from rest_framework.compat import patterns
-
-urlpatterns = patterns('',
-)
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 682a99a4..2eef6eeb 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -6,20 +6,31 @@ form encoded input.
Serialization in REST framework is a two-phase process:
1. Serializers marshal between complex types like model instances, and
-python primatives.
-2. The process of marshalling between python primatives and request and
+python primitives.
+2. The process of marshalling between python primitives and request and
response content is handled by parsers and renderers.
"""
from __future__ import unicode_literals
-import copy
-import datetime
-import types
-from decimal import Decimal
-from django.core.paginator import Page
from django.db import models
-from django.forms import widgets
-from django.utils.datastructures import SortedDict
-from rest_framework.compat import get_concrete_model, six
+from django.db.models.fields import FieldDoesNotExist, Field as DjangoModelField
+from django.db.models import query
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import postgres_fields, unicode_to_repr
+from rest_framework.utils import model_meta
+from rest_framework.utils.field_mapping import (
+ get_url_kwargs, get_field_kwargs,
+ get_relation_kwargs, get_nested_relation_kwargs,
+ ClassLookupDict
+)
+from rest_framework.utils.serializer_helpers import (
+ ReturnDict, ReturnList, BoundField, NestedBoundField, BindingDict
+)
+from rest_framework.validators import (
+ UniqueForDateValidator, UniqueForMonthValidator, UniqueForYearValidator,
+ UniqueTogetherValidator
+)
+import warnings
+
# Note: We do the following so that users of the framework can use this style:
#
@@ -28,941 +39,1354 @@ from rest_framework.compat import get_concrete_model, six
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
-from rest_framework.relations import *
-from rest_framework.fields import *
+from rest_framework.relations import * # NOQA
+from rest_framework.fields import * # NOQA
-class NestedValidationError(ValidationError):
- """
- The default ValidationError behavior is to stringify each item in the list
- if the messages are a list of error messages.
+# We assume that 'validators' are intended for the child serializer,
+# rather than the parent serializer.
+LIST_SERIALIZER_KWARGS = (
+ 'read_only', 'write_only', 'required', 'default', 'initial', 'source',
+ 'label', 'help_text', 'style', 'error_messages',
+ 'instance', 'data', 'partial', 'context'
+)
+
- In the case of nested serializers, where the parent has many children,
- then the child's `serializer.errors` will be a list of dicts. In the case
- of a single child, the `serializer.errors` will be a dict.
+# BaseSerializer
+# --------------
- We need to override the default behavior to get properly nested error dicts.
+class BaseSerializer(Field):
"""
+ The BaseSerializer class provides a minimal class which may be used
+ for writing custom serializer implementations.
- def __init__(self, message):
- if isinstance(message, dict):
- self.messages = [message]
- else:
- self.messages = message
+ Note that we strongly restrict the ordering of operations/properties
+ that may be used on the serializer in order to enforce correct usage.
+ In particular, if a `data=` argument is passed then:
-class DictWithMetadata(dict):
- """
- A dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
- """
- Used by pickle (e.g., caching).
- Overridden to remove the metadata from the dict, since it shouldn't be
- pickled and may in some instances be unpickleable.
- """
- return dict(self)
+ .is_valid() - Available.
+ .initial_data - Available.
+ .validated_data - Only available after calling `is_valid()`
+ .errors - Only available after calling `is_valid()`
+ .data - Only available after calling `is_valid()`
+ If a `data=` argument is not passed then:
-class SortedDictWithMetadata(SortedDict):
+ .is_valid() - Not available.
+ .initial_data - Not available.
+ .validated_data - Not available.
+ .errors - Not available.
+ .data - Available.
"""
- A sorted dict-like object, that can have additional properties attached.
- """
- def __getstate__(self):
+
+ def __init__(self, instance=None, data=empty, **kwargs):
+ self.instance = instance
+ if data is not empty:
+ self.initial_data = data
+ self.partial = kwargs.pop('partial', False)
+ self._context = kwargs.pop('context', {})
+ kwargs.pop('many', None)
+ super(BaseSerializer, self).__init__(**kwargs)
+
+ def __new__(cls, *args, **kwargs):
+ # We override this method in order to automagically create
+ # `ListSerializer` classes instead when `many=True` is set.
+ if kwargs.pop('many', False):
+ return cls.many_init(*args, **kwargs)
+ return super(BaseSerializer, cls).__new__(cls, *args, **kwargs)
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
"""
- Used by pickle (e.g., caching).
- Overriden to remove the metadata from the dict, since it shouldn't be
- pickle and may in some instances be unpickleable.
+ This method implements the creation of a `ListSerializer` parent
+ class when `many=True` is used. You can customize it if you need to
+ control which keyword arguments are passed to the parent, and
+ which are passed to the child.
+
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomListSerializer(*args, **kwargs)
"""
- return SortedDict(self).__dict__
+ child_serializer = cls(*args, **kwargs)
+ list_kwargs = {'child': child_serializer}
+ list_kwargs.update(dict([
+ (key, value) for key, value in kwargs.items()
+ if key in LIST_SERIALIZER_KWARGS
+ ]))
+ meta = getattr(cls, 'Meta', None)
+ list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
+ return list_serializer_class(*args, **list_kwargs)
+ def to_internal_value(self, data):
+ raise NotImplementedError('`to_internal_value()` must be implemented.')
-def _is_protected_type(obj):
- """
- True if the object is a native datatype that does not need to
- be serialized further.
- """
- return isinstance(obj, (
- types.NoneType,
- int, long,
- datetime.datetime, datetime.date, datetime.time,
- float, Decimal,
- basestring)
- )
+ def to_representation(self, instance):
+ raise NotImplementedError('`to_representation()` must be implemented.')
+ def update(self, instance, validated_data):
+ raise NotImplementedError('`update()` must be implemented.')
-def _get_declared_fields(bases, attrs):
- """
- Create a list of serializer field instances from the passed in 'attrs',
- plus any fields on the base classes (in 'bases').
+ def create(self, validated_data):
+ raise NotImplementedError('`create()` must be implemented.')
- Note that all fields from the base classes are used.
- """
- fields = [(field_name, attrs.pop(field_name))
- for field_name, obj in list(six.iteritems(attrs))
- if isinstance(obj, Field)]
- fields.sort(key=lambda x: x[1].creation_counter)
+ def save(self, **kwargs):
+ assert not hasattr(self, 'save_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.save_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
- # If this class is subclassing another Serializer, add that Serializer's
- # fields. Note that we loop over the bases in *reverse*. This is necessary
- # in order to maintain the correct order of fields.
- for base in bases[::-1]:
- if hasattr(base, 'base_fields'):
- fields = list(base.base_fields.items()) + fields
+ assert hasattr(self, '_errors'), (
+ 'You must call `.is_valid()` before calling `.save()`.'
+ )
- return SortedDict(fields)
+ assert not self.errors, (
+ 'You cannot call `.save()` on a serializer with invalid data.'
+ )
+ validated_data = dict(
+ list(self.validated_data.items()) +
+ list(kwargs.items())
+ )
-class SerializerMetaclass(type):
- def __new__(cls, name, bases, attrs):
- attrs['base_fields'] = _get_declared_fields(bases, attrs)
- return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
+ else:
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
+ return self.instance
-class SerializerOptions(object):
- """
- Meta class options for Serializer
- """
- def __init__(self, meta):
- self.depth = getattr(meta, 'depth', 0)
- self.fields = getattr(meta, 'fields', ())
- self.exclude = getattr(meta, 'exclude', ())
+ def is_valid(self, raise_exception=False):
+ assert not hasattr(self, 'restore_object'), (
+ 'Serializer `%s.%s` has old-style version 2 `.restore_object()` '
+ 'that is no longer compatible with REST framework 3. '
+ 'Use the new-style `.create()` and `.update()` methods instead.' %
+ (self.__class__.__module__, self.__class__.__name__)
+ )
+ assert hasattr(self, 'initial_data'), (
+ 'Cannot call `.is_valid()` as no `data=` keyword argument was '
+ 'passed when instantiating the serializer instance.'
+ )
-class BaseSerializer(WritableField):
- """
- This is the Serializer implementation.
- We need to implement it as `BaseSerializer` due to metaclass magicks.
- """
- class Meta(object):
- pass
+ if not hasattr(self, '_validated_data'):
+ try:
+ self._validated_data = self.run_validation(self.initial_data)
+ except ValidationError as exc:
+ self._validated_data = {}
+ self._errors = exc.detail
+ else:
+ self._errors = {}
- _options_class = SerializerOptions
- _dict_class = SortedDictWithMetadata
+ if self._errors and raise_exception:
+ raise ValidationError(self._errors)
- def __init__(self, instance=None, data=None, files=None,
- context=None, partial=False, many=None,
- allow_add_remove=False, **kwargs):
- super(BaseSerializer, self).__init__(**kwargs)
- self.opts = self._options_class(self.Meta)
- self.parent = None
- self.root = None
- self.partial = partial
- self.many = many
- self.allow_add_remove = allow_add_remove
+ return not bool(self._errors)
- self.context = context or {}
+ @property
+ def data(self):
+ if hasattr(self, 'initial_data') and not hasattr(self, '_validated_data'):
+ msg = (
+ 'When a serializer is passed a `data` keyword argument you '
+ 'must call `.is_valid()` before attempting to access the '
+ 'serialized `.data` representation.\n'
+ 'You should either call `.is_valid()` first, '
+ 'or access `.initial_data` instead.'
+ )
+ raise AssertionError(msg)
- self.init_data = data
- self.init_files = files
- self.object = instance
- self.fields = self.get_fields()
+ if not hasattr(self, '_data'):
+ if self.instance is not None and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.instance)
+ elif hasattr(self, '_validated_data') and not getattr(self, '_errors', None):
+ self._data = self.to_representation(self.validated_data)
+ else:
+ self._data = self.get_initial()
+ return self._data
- self._data = None
- self._files = None
- self._errors = None
- self._deleted = None
+ @property
+ def errors(self):
+ if not hasattr(self, '_errors'):
+ msg = 'You must call `.is_valid()` before accessing `.errors`.'
+ raise AssertionError(msg)
+ return self._errors
- if many and instance is not None and not hasattr(instance, '__iter__'):
- raise ValueError('instance should be a queryset or other iterable with many=True')
+ @property
+ def validated_data(self):
+ if not hasattr(self, '_validated_data'):
+ msg = 'You must call `.is_valid()` before accessing `.validated_data`.'
+ raise AssertionError(msg)
+ return self._validated_data
- if allow_add_remove and not many:
- raise ValueError('allow_add_remove should only be used for bulk updates, but you have not set many=True')
- #####
- # Methods to determine which fields to use when (de)serializing objects.
+# Serializer & ListSerializer classes
+# -----------------------------------
- def get_default_fields(self):
- """
- Return the complete set of default fields for the object, as a dict.
- """
- return {}
+class SerializerMetaclass(type):
+ """
+ This metaclass sets a dictionary named `_declared_fields` on the class.
- def get_fields(self):
- """
- Returns the complete set of fields for the object as a dict.
+ Any instances of `Field` included as attributes on either the class
+ or on any of its superclasses will be include in the
+ `_declared_fields` dictionary.
+ """
- This will be the set of any explicitly declared fields,
- plus the set of fields returned by get_default_fields().
- """
- ret = SortedDict()
+ @classmethod
+ def _get_declared_fields(cls, bases, attrs):
+ fields = [(field_name, attrs.pop(field_name))
+ for field_name, obj in list(attrs.items())
+ if isinstance(obj, Field)]
+ fields.sort(key=lambda x: x[1]._creation_counter)
- # Get the explicitly declared fields
- base_fields = copy.deepcopy(self.base_fields)
- for key, field in base_fields.items():
- ret[key] = field
+ # If this class is subclassing another Serializer, add that Serializer's
+ # fields. Note that we loop over the bases in *reverse*. This is necessary
+ # in order to maintain the correct order of fields.
+ for base in reversed(bases):
+ if hasattr(base, '_declared_fields'):
+ fields = list(base._declared_fields.items()) + fields
- # Add in the default fields
- default_fields = self.get_default_fields()
- for key, val in default_fields.items():
- if key not in ret:
- ret[key] = val
+ return OrderedDict(fields)
- # If 'fields' is specified, use those fields, in that order.
- if self.opts.fields:
- assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
- new = SortedDict()
- for key in self.opts.fields:
- new[key] = ret[key]
- ret = new
+ def __new__(cls, name, bases, attrs):
+ attrs['_declared_fields'] = cls._get_declared_fields(bases, attrs)
+ return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs)
- # Remove anything in 'exclude'
- if self.opts.exclude:
- assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
- for key in self.opts.exclude:
- ret.pop(key, None)
- for key, field in ret.items():
- field.initialize(parent=self, field_name=key)
+def get_validation_error_detail(exc):
+ assert isinstance(exc, (ValidationError, DjangoValidationError))
- return ret
+ if isinstance(exc, DjangoValidationError):
+ # Normally you should raise `serializers.ValidationError`
+ # inside your codebase, but we handle Django's validation
+ # exception class as well for simpler compat.
+ # Eg. Calling Model.clean() explicitly inside Serializer.validate()
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages)
+ }
+ elif isinstance(exc.detail, dict):
+ # If errors may be a dict we use the standard {key: list of values}.
+ # Here we ensure that all the values are *lists* of errors.
+ return dict([
+ (key, value if isinstance(value, list) else [value])
+ for key, value in exc.detail.items()
+ ])
+ elif isinstance(exc.detail, list):
+ # Errors raised as a list are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: exc.detail
+ }
+ # Errors raised as a string are non-field errors.
+ return {
+ api_settings.NON_FIELD_ERRORS_KEY: [exc.detail]
+ }
- #####
- # Methods to convert or revert from objects <--> primitive representations.
- def get_field_key(self, field_name):
+@six.add_metaclass(SerializerMetaclass)
+class Serializer(BaseSerializer):
+ default_error_messages = {
+ 'invalid': _('Invalid data. Expected a dictionary, but got {datatype}.')
+ }
+
+ @property
+ def fields(self):
"""
- Return the key that should be used for a given field.
+ A dictionary of {field_name: field_instance}.
"""
- return field_name
+ # `fields` is evaluated lazily. We do this to ensure that we don't
+ # have issues importing modules that use ModelSerializers as fields,
+ # even if Django's app-loading stage has not yet run.
+ if not hasattr(self, '_fields'):
+ self._fields = BindingDict(self)
+ for key, value in self.get_fields().items():
+ self._fields[key] = value
+ return self._fields
- def restore_fields(self, data, files):
+ def get_fields(self):
"""
- Core of deserialization, together with `restore_object`.
- Converts a dictionary of data into a dictionary of deserialized fields.
+ Returns a dictionary of {field_name: field_instance}.
"""
- reverted_data = {}
+ # Every new serializer is created with a clone of the field instances.
+ # This allows users to dynamically modify the fields on a serializer
+ # instance without affecting every other serializer class.
+ return copy.deepcopy(self._declared_fields)
- if data is not None and not isinstance(data, dict):
- self._errors['non_field_errors'] = ['Invalid data']
- return None
+ def get_validators(self):
+ """
+ Returns a list of validator callables.
+ """
+ # Used by the lazily-evaluated `validators` property.
+ meta = getattr(self, 'Meta', None)
+ validators = getattr(meta, 'validators', None)
+ return validators[:] if validators else []
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return OrderedDict([
+ (field_name, field.get_value(self.initial_data))
+ for field_name, field in self.fields.items()
+ if (field.get_value(self.initial_data) is not empty) and
+ not field.read_only
+ ])
+
+ return OrderedDict([
+ (field.field_name, field.get_initial())
+ for field in self.fields.values()
+ if not field.read_only
+ ])
+
+ def get_value(self, dictionary):
+ # We override the default field access in order to support
+ # nested HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_dict(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
+
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
+ """
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- try:
- field.field_from_native(data, files, field_name, reverted_data)
- except ValidationError as err:
- self._errors[field_name] = list(err.messages)
+ value = self.to_internal_value(data)
+ try:
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
- return reverted_data
+ return value
- def perform_validation(self, attrs):
+ def to_internal_value(self, data):
"""
- Run `validate_<fieldname>()` and `validate()` methods on the serializer
+ Dict of native values <- Dict of primitive datatypes.
"""
- for field_name, field in self.fields.items():
- if field_name in self._errors:
- continue
- try:
- validate_method = getattr(self, 'validate_%s' % field_name, None)
- if validate_method:
- source = field.source or field_name
- attrs = validate_method(attrs, source)
- except ValidationError as err:
- self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages)
-
- # If there are already errors, we don't run .validate() because
- # field-validation failed and thus `attrs` may not be complete.
- # which in turn can cause inconsistent validation errors.
- if not self._errors:
+ if not isinstance(data, dict):
+ message = self.error_messages['invalid'].format(
+ datatype=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
+
+ ret = OrderedDict()
+ errors = OrderedDict()
+ fields = [
+ field for field in self.fields.values()
+ if (not field.read_only) or (field.default is not empty)
+ ]
+
+ for field in fields:
+ validate_method = getattr(self, 'validate_' + field.field_name, None)
+ primitive_value = field.get_value(data)
try:
- attrs = self.validate(attrs)
- except ValidationError as err:
- if hasattr(err, 'message_dict'):
- for field_name, error_messages in err.message_dict.items():
- self._errors[field_name] = self._errors.get(field_name, []) + list(error_messages)
- elif hasattr(err, 'messages'):
- self._errors['non_field_errors'] = err.messages
+ validated_value = field.run_validation(primitive_value)
+ if validate_method is not None:
+ validated_value = validate_method(validated_value)
+ except ValidationError as exc:
+ errors[field.field_name] = exc.detail
+ except DjangoValidationError as exc:
+ errors[field.field_name] = list(exc.messages)
+ except SkipField:
+ pass
+ else:
+ set_value(ret, field.source_attrs, validated_value)
- return attrs
+ if errors:
+ raise ValidationError(errors)
- def validate(self, attrs):
- """
- Stub method, to be overridden in Serializer subclasses
- """
- return attrs
+ return ret
- def restore_object(self, attrs, instance=None):
+ def to_representation(self, instance):
"""
- Deserialize a dictionary of attributes into an object instance.
- You should override this method to control how deserialized objects
- are instantiated.
+ Object instance -> Dict of primitive datatypes.
"""
- if instance is not None:
- instance.update(attrs)
- return instance
- return attrs
+ ret = OrderedDict()
+ fields = [field for field in self.fields.values() if not field.write_only]
- def to_native(self, obj):
- """
- Serialize objects -> primitives.
- """
- ret = self._dict_class()
- ret.fields = {}
+ for field in fields:
+ try:
+ attribute = field.get_attribute(instance)
+ except SkipField:
+ continue
+
+ if attribute is None:
+ # We skip `to_representation` for `None` values so that
+ # fields do not have to explicitly deal with that case.
+ ret[field.field_name] = None
+ else:
+ ret[field.field_name] = field.to_representation(attribute)
- for field_name, field in self.fields.items():
- field.initialize(parent=self, field_name=field_name)
- key = self.get_field_key(field_name)
- value = field.field_to_native(obj, field_name)
- ret[key] = value
- ret.fields[key] = field
return ret
- def from_native(self, data, files):
- """
- Deserialize primitives -> objects.
- """
- self._errors = {}
- if data is not None or files is not None:
- attrs = self.restore_fields(data, files)
- if attrs is not None:
- attrs = self.perform_validation(attrs)
- else:
- self._errors['non_field_errors'] = ['No input provided']
+ def validate(self, attrs):
+ return attrs
- if not self._errors:
- return self.restore_object(attrs, instance=getattr(self, 'object', None))
+ def __repr__(self):
+ return unicode_to_repr(representation.serializer_repr(self, indent=1))
- def field_to_native(self, obj, field_name):
- """
- Override default so that the serializer can be used as a nested field
- across relationships.
- """
- if self.source == '*':
- return self.to_native(obj)
+ # The following are used for accessing `BoundField` instances on the
+ # serializer, for the purposes of presenting a form-like API onto the
+ # field values and field errors.
- try:
- source = self.source or field_name
- value = obj
+ def __iter__(self):
+ for field in self.fields.values():
+ yield self[field.field_name]
- for component in source.split('.'):
- value = get_component(value, component)
- if value is None:
- break
- except ObjectDoesNotExist:
- return None
+ def __getitem__(self, key):
+ field = self.fields[key]
+ value = self.data.get(key)
+ error = self.errors.get(key) if hasattr(self, '_errors') else None
+ if isinstance(field, Serializer):
+ return NestedBoundField(field, value, error)
+ return BoundField(field, value, error)
- if is_simple_callable(getattr(value, 'all', None)):
- return [self.to_native(item) for item in value.all()]
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
- if value is None:
- return None
+ @property
+ def data(self):
+ ret = super(Serializer, self).data
+ return ReturnDict(ret, serializer=self)
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(value, '__iter__') and not isinstance(value, (Page, dict, six.text_type))
+ @property
+ def errors(self):
+ ret = super(Serializer, self).errors
+ return ReturnDict(ret, serializer=self)
- if many:
- return [self.to_native(item) for item in value]
- return self.to_native(value)
- def field_from_native(self, data, files, field_name, into):
- """
- Override default so that the serializer can be used as a writable
- nested field across relationships.
- """
- if self.read_only:
- return
+# There's some replication of `ListField` here,
+# but that's probably better than obfuscating the call hierarchy.
- try:
- value = data[field_name]
- except KeyError:
- if self.default is not None and not self.partial:
- # Note: partial updates shouldn't set defaults
- value = copy.deepcopy(self.default)
- else:
- if self.required:
- raise ValidationError(self.error_messages['required'])
- return
+class ListSerializer(BaseSerializer):
+ child = None
+ many = True
- # Set the serializer object if it exists
- obj = getattr(self.parent.object, field_name) if self.parent.object else None
+ default_error_messages = {
+ 'not_a_list': _('Expected a list of items but got type "{input_type}".')
+ }
- if self.source == '*':
- if value:
- into.update(value)
- else:
- if value in (None, ''):
- into[(self.source or field_name)] = None
- else:
- kwargs = {
- 'instance': obj,
- 'data': value,
- 'context': self.context,
- 'partial': self.partial,
- 'many': self.many
- }
- serializer = self.__class__(**kwargs)
-
- if serializer.is_valid():
- into[self.source or field_name] = serializer.object
- else:
- # Propagate errors up to our parent
- raise NestedValidationError(serializer.errors)
+ def __init__(self, *args, **kwargs):
+ self.child = kwargs.pop('child', copy.deepcopy(self.child))
+ assert self.child is not None, '`child` is a required argument.'
+ assert not inspect.isclass(self.child), '`child` has not been instantiated.'
+ super(ListSerializer, self).__init__(*args, **kwargs)
+ self.child.bind(field_name='', parent=self)
+
+ def get_initial(self):
+ if hasattr(self, 'initial_data'):
+ return self.to_representation(self.initial_data)
+ return []
- def get_identity(self, data):
+ def get_value(self, dictionary):
"""
- This hook is required for bulk update.
- It is used to determine the canonical identity of a given object.
+ Given the input dictionary, return the field value.
+ """
+ # We override the default field access in order to support
+ # lists in HTML forms.
+ if html.is_html_input(dictionary):
+ return html.parse_html_list(dictionary, prefix=self.field_name)
+ return dictionary.get(self.field_name, empty)
- Note that the data has not been validated at this point, so we need
- to make sure that we catch any cases of incorrect datatypes being
- passed to this method.
+ def run_validation(self, data=empty):
+ """
+ We override the default `run_validation`, because the validation
+ performed by validators and the `.validate()` method should
+ be coerced into an error dictionary with a 'non_fields_error' key.
"""
+ (is_empty_value, data) = self.validate_empty_values(data)
+ if is_empty_value:
+ return data
+
+ value = self.to_internal_value(data)
try:
- return data.get('id', None)
- except AttributeError:
- return None
+ self.run_validators(value)
+ value = self.validate(value)
+ assert value is not None, '.validate() should return the validated data'
+ except (ValidationError, DjangoValidationError) as exc:
+ raise ValidationError(detail=get_validation_error_detail(exc))
- @property
- def errors(self):
+ return value
+
+ def to_internal_value(self, data):
"""
- Run deserialization and return error data,
- setting self.object if no errors occurred.
+ List of dicts of native values <- List of dicts of primitive datatypes.
"""
- if self._errors is None:
- data, files = self.init_data, self.init_files
+ if html.is_html_input(data):
+ data = html.parse_html_list(data)
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=3)
-
- if many:
- ret = []
- errors = []
- update = self.object is not None
-
- if update:
- # If this is a bulk update we need to map all the objects
- # to a canonical identity so we can determine which
- # individual object is being updated for each item in the
- # incoming data
- objects = self.object
- identities = [self.get_identity(self.to_native(obj)) for obj in objects]
- identity_to_objects = dict(zip(identities, objects))
-
- if hasattr(data, '__iter__') and not isinstance(data, (dict, six.text_type)):
- for item in data:
- if update:
- # Determine which object we're updating
- identity = self.get_identity(item)
- self.object = identity_to_objects.pop(identity, None)
- if self.object is None and not self.allow_add_remove:
- ret.append(None)
- errors.append({'non_field_errors': ['Cannot create a new item, only existing items may be updated.']})
- continue
-
- ret.append(self.from_native(item, None))
- errors.append(self._errors)
-
- if update:
- self._deleted = identity_to_objects.values()
-
- self._errors = any(errors) and errors or []
- else:
- self._errors = {'non_field_errors': ['Expected a list of items.']}
- else:
- ret = self.from_native(data, files)
+ if not isinstance(data, list):
+ message = self.error_messages['not_a_list'].format(
+ input_type=type(data).__name__
+ )
+ raise ValidationError({
+ api_settings.NON_FIELD_ERRORS_KEY: [message]
+ })
- if not self._errors:
- self.object = ret
+ ret = []
+ errors = []
- return self._errors
+ for item in data:
+ try:
+ validated = self.child.run_validation(item)
+ except ValidationError as exc:
+ errors.append(exc.detail)
+ else:
+ ret.append(validated)
+ errors.append({})
- def is_valid(self):
- return not self.errors
+ if any(errors):
+ raise ValidationError(errors)
- @property
- def data(self):
+ return ret
+
+ def to_representation(self, data):
"""
- Returns the serialized data on the serializer.
+ List of object instances -> List of dicts of primitive datatypes.
"""
- if self._data is None:
- obj = self.object
+ # Dealing with nested relationships, data can be a Manager,
+ # so, first get a queryset from the Manager if needed
+ iterable = data.all() if isinstance(data, (models.Manager, query.QuerySet)) else data
+ return [
+ self.child.to_representation(item) for item in iterable
+ ]
- if self.many is not None:
- many = self.many
- else:
- many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict))
- if many:
- warnings.warn('Implict list/queryset serialization is deprecated. '
- 'Use the `many=True` flag when instantiating the serializer.',
- DeprecationWarning, stacklevel=2)
-
- if many:
- self._data = [self.to_native(item) for item in obj]
- else:
- self._data = self.to_native(obj)
-
- return self._data
+ def validate(self, attrs):
+ return attrs
- def save_object(self, obj, **kwargs):
- obj.save(**kwargs)
+ def update(self, instance, validated_data):
+ raise NotImplementedError(
+ "Serializers with many=True do not support multiple update by "
+ "default, only multiple create. For updates it is unclear how to "
+ "deal with insertions and deletions. If you need to support "
+ "multiple update, use a `ListSerializer` class and override "
+ "`.update()` so you can specify the behavior exactly."
+ )
- def delete_object(self, obj):
- obj.delete()
+ def create(self, validated_data):
+ return [
+ self.child.create(attrs) for attrs in validated_data
+ ]
def save(self, **kwargs):
"""
- Save the deserialized object and return it.
+ Save and return a list of object instances.
"""
- if isinstance(self.object, list):
- [self.save_object(item, **kwargs) for item in self.object]
+ validated_data = [
+ dict(list(attrs.items()) + list(kwargs.items()))
+ for attrs in self.validated_data
+ ]
+
+ if self.instance is not None:
+ self.instance = self.update(self.instance, validated_data)
+ assert self.instance is not None, (
+ '`update()` did not return an object instance.'
+ )
else:
- self.save_object(self.object, **kwargs)
+ self.instance = self.create(validated_data)
+ assert self.instance is not None, (
+ '`create()` did not return an object instance.'
+ )
- if self.allow_add_remove and self._deleted:
- [self.delete_object(item) for item in self._deleted]
+ return self.instance
- return self.object
+ def __repr__(self):
+ return unicode_to_repr(representation.list_repr(self, indent=1))
- def metadata(self):
- """
- Return a dictionary of metadata about the fields on the serializer.
- Useful for things like responding to OPTIONS requests, or generating
- API schemas for auto-documentation.
- """
- return SortedDict(
- [(field_name, field.metadata())
- for field_name, field in six.iteritems(self.fields)]
- )
+ # Include a backlink to the serializer class on return objects.
+ # Allows renderers such as HTMLFormRenderer to get the full field info.
+
+ @property
+ def data(self):
+ ret = super(ListSerializer, self).data
+ return ReturnList(ret, serializer=self)
+ @property
+ def errors(self):
+ ret = super(ListSerializer, self).errors
+ if isinstance(ret, dict):
+ return ReturnDict(ret, serializer=self)
+ return ReturnList(ret, serializer=self)
-class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
- pass
+# ModelSerializer & HyperlinkedModelSerializer
+# --------------------------------------------
-class ModelSerializerOptions(SerializerOptions):
+def raise_errors_on_nested_writes(method_name, serializer, validated_data):
"""
- Meta class options for ModelSerializer
+ Give explicit errors when users attempt to pass writable nested data.
+
+ If we don't do this explicitly they'd get a less helpful error when
+ calling `.save()` on the serializer.
+
+ We don't *automatically* support these sorts of nested writes because
+ there are too many ambiguities to define a default behavior.
+
+ Eg. Suppose we have a `UserSerializer` with a nested profile. How should
+ we handle the case of an update, where the `profile` relationship does
+ not exist? Any of the following might be valid:
+
+ * Raise an application error.
+ * Silently ignore the nested part of the update.
+ * Automatically create a profile instance.
"""
- def __init__(self, meta):
- super(ModelSerializerOptions, self).__init__(meta)
- self.model = getattr(meta, 'model', None)
- self.read_only_fields = getattr(meta, 'read_only_fields', ())
+
+ # Ensure we don't have a writable nested field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # profile = ProfileSerializer()
+ assert not any(
+ isinstance(field, BaseSerializer) and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable nested'
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'nested serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
+
+ # Ensure we don't have a writable dotted-source field. For example:
+ #
+ # class UserSerializer(ModelSerializer):
+ # ...
+ # address = serializer.CharField('profile.address')
+ assert not any(
+ '.' in field.source and
+ (key in validated_data) and
+ isinstance(validated_data[key], (list, dict))
+ for key, field in serializer.fields.items()
+ ), (
+ 'The `.{method_name}()` method does not support writable dotted-source '
+ 'fields by default.\nWrite an explicit `.{method_name}()` method for '
+ 'serializer `{module}.{class_name}`, or set `read_only=True` on '
+ 'dotted-source serializer fields.'.format(
+ method_name=method_name,
+ module=serializer.__class__.__module__,
+ class_name=serializer.__class__.__name__
+ )
+ )
class ModelSerializer(Serializer):
"""
- A serializer that deals with model instances and querysets.
- """
- _options_class = ModelSerializerOptions
+ A `ModelSerializer` is just a regular `Serializer`, except that:
+
+ * A set of default fields are automatically populated.
+ * A set of default validators are automatically populated.
+ * Default `.create()` and `.update()` implementations are provided.
+
+ The process of automatically determining a set of serializer fields
+ based on the model fields is reasonably complex, but you almost certainly
+ don't need to dig into the implementation.
- field_mapping = {
+ If the `ModelSerializer` class *doesn't* generate the set of fields that
+ you need you should either declare the extra/differing fields explicitly on
+ the serializer class, or simply use a `Serializer` class.
+ """
+ serializer_field_mapping = {
models.AutoField: IntegerField,
+ models.BigIntegerField: IntegerField,
+ models.BooleanField: BooleanField,
+ models.CharField: CharField,
+ models.CommaSeparatedIntegerField: CharField,
+ models.DateField: DateField,
+ models.DateTimeField: DateTimeField,
+ models.DecimalField: DecimalField,
+ models.EmailField: EmailField,
+ models.Field: ModelField,
+ models.FileField: FileField,
models.FloatField: FloatField,
+ models.ImageField: ImageField,
models.IntegerField: IntegerField,
+ models.NullBooleanField: NullBooleanField,
models.PositiveIntegerField: IntegerField,
- models.SmallIntegerField: IntegerField,
models.PositiveSmallIntegerField: IntegerField,
- models.DateTimeField: DateTimeField,
- models.DateField: DateField,
- models.TimeField: TimeField,
- models.DecimalField: DecimalField,
- models.EmailField: EmailField,
- models.CharField: CharField,
- models.URLField: URLField,
models.SlugField: SlugField,
+ models.SmallIntegerField: IntegerField,
models.TextField: CharField,
- models.CommaSeparatedIntegerField: CharField,
- models.BooleanField: BooleanField,
- models.FileField: FileField,
- models.ImageField: ImageField,
+ models.TimeField: TimeField,
+ models.URLField: URLField,
}
+ serializer_related_field = PrimaryKeyRelatedField
+ serializer_url_field = HyperlinkedIdentityField
+ serializer_choice_field = ChoiceField
+
+ # Default `create` and `update` behavior...
- def get_default_fields(self):
+ def create(self, validated_data):
"""
- Return all the fields that should be serialized for the model.
+ We have a bit of extra checking around this in order to provide
+ descriptive messages when something goes wrong, but this method is
+ essentially just:
+
+ return ExampleModel.objects.create(**validated_data)
+
+ If there are many to many fields present on the instance then they
+ cannot be set until the model is instantiated, in which case the
+ implementation is like so:
+
+ example_relationship = validated_data.pop('example_relationship')
+ instance = ExampleModel.objects.create(**validated_data)
+ instance.example_relationship = example_relationship
+ return instance
+
+ The default implementation also does not handle nested relationships.
+ If you want to support writable nested relationships you'll need
+ to write an explicit `.create()` method.
"""
+ raise_errors_on_nested_writes('create', self, validated_data)
- cls = self.opts.model
- assert cls is not None, \
- "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
- opts = get_concrete_model(cls)._meta
- ret = SortedDict()
- nested = bool(self.opts.depth)
+ ModelClass = self.Meta.model
- # Deal with adding the primary key field
- pk_field = opts.pk
- while pk_field.rel and pk_field.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk
- pk_field = pk_field.rel.to._meta.pk
+ # Remove many-to-many relationships from validated_data.
+ # They are not valid arguments to the default `.create()` method,
+ # as they require that the instance has already been saved.
+ info = model_meta.get_field_info(ModelClass)
+ many_to_many = {}
+ for field_name, relation_info in info.relations.items():
+ if relation_info.to_many and (field_name in validated_data):
+ many_to_many[field_name] = validated_data.pop(field_name)
- field = self.get_pk_field(pk_field)
- if field:
- ret[pk_field.name] = field
+ try:
+ instance = ModelClass.objects.create(**validated_data)
+ except TypeError as exc:
+ msg = (
+ 'Got a `TypeError` when calling `%s.objects.create()`. '
+ 'This may be because you have a writable field on the '
+ 'serializer class that is not a valid argument to '
+ '`%s.objects.create()`. You may need to make the field '
+ 'read-only, or override the %s.create() method to handle '
+ 'this correctly.\nOriginal exception text was: %s.' %
+ (
+ ModelClass.__name__,
+ ModelClass.__name__,
+ self.__class__.__name__,
+ exc
+ )
+ )
+ raise TypeError(msg)
- # Deal with forward relationships
- forward_rels = [field for field in opts.fields if field.serialize]
- forward_rels += [field for field in opts.many_to_many if field.serialize]
+ # Save many-to-many relationships after the instance is created.
+ if many_to_many:
+ for field_name, value in many_to_many.items():
+ setattr(instance, field_name, value)
- for model_field in forward_rels:
- has_through_model = False
+ return instance
- if model_field.rel:
- to_many = isinstance(model_field,
- models.fields.related.ManyToManyField)
- related_model = model_field.rel.to
+ def update(self, instance, validated_data):
+ raise_errors_on_nested_writes('update', self, validated_data)
- if to_many and not model_field.rel.through._meta.auto_created:
- has_through_model = True
+ for attr, value in validated_data.items():
+ setattr(instance, attr, value)
+ instance.save()
- if model_field.rel and nested:
- if len(inspect.getargspec(self.get_nested_field).args) == 2:
- warnings.warn(
- 'The `get_nested_field(model_field)` call signature '
- 'is due to be deprecated. '
- 'Use `get_nested_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_nested_field(model_field)
- else:
- field = self.get_nested_field(model_field, related_model, to_many)
- elif model_field.rel:
- if len(inspect.getargspec(self.get_nested_field).args) == 3:
- warnings.warn(
- 'The `get_related_field(model_field, to_many)` call '
- 'signature is due to be deprecated. '
- 'Use `get_related_field(model_field, related_model, '
- 'to_many) instead',
- PendingDeprecationWarning
- )
- field = self.get_related_field(model_field, to_many=to_many)
- else:
- field = self.get_related_field(model_field, related_model, to_many)
- else:
- field = self.get_field(model_field)
+ return instance
- if field:
- if has_through_model:
- field.read_only = True
+ # Determine the fields to apply...
- ret[model_field.name] = field
+ def get_fields(self):
+ """
+ Return the dict of field names -> field instances that should be
+ used for `self.fields` when instantiating the serializer.
+ """
+ assert hasattr(self, 'Meta'), (
+ 'Class {serializer_class} missing "Meta" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+ assert hasattr(self.Meta, 'model'), (
+ 'Class {serializer_class} missing "Meta.model" attribute'.format(
+ serializer_class=self.__class__.__name__
+ )
+ )
- # Deal with reverse relationships
- if not self.opts.fields:
- reverse_rels = []
- else:
- # Reverse relationships are only included if they are explicitly
- # present in the `fields` option on the serializer
- reverse_rels = opts.get_all_related_objects()
- reverse_rels += opts.get_all_related_many_to_many_objects()
-
- for relation in reverse_rels:
- accessor_name = relation.get_accessor_name()
- if not self.opts.fields or accessor_name not in self.opts.fields:
+ declared_fields = copy.deepcopy(self._declared_fields)
+ model = getattr(self.Meta, 'model')
+ depth = getattr(self.Meta, 'depth', 0)
+
+ if depth is not None:
+ assert depth >= 0, "'depth' may not be negative."
+ assert depth <= 10, "'depth' may not be greater than 10."
+
+ # Retrieve metadata about fields & relationships on the model class.
+ info = model_meta.get_field_info(model)
+ field_names = self.get_field_names(declared_fields, info)
+
+ # Determine any extra field arguments and hidden fields that
+ # should be included
+ extra_kwargs = self.get_extra_kwargs()
+ extra_kwargs, hidden_fields = self.get_uniqueness_extra_kwargs(
+ field_names, declared_fields, extra_kwargs
+ )
+
+ # Determine the fields that should be included on the serializer.
+ fields = OrderedDict()
+
+ for field_name in field_names:
+ # If the field is explicitly declared on the class then use that.
+ if field_name in declared_fields:
+ fields[field_name] = declared_fields[field_name]
continue
- related_model = relation.model
- to_many = relation.field.rel.multiple
- has_through_model = False
- is_m2m = isinstance(relation.field,
- models.fields.related.ManyToManyField)
- if is_m2m and not relation.field.rel.through._meta.auto_created:
- has_through_model = True
+ # Determine the serializer field class and keyword arguments.
+ field_class, field_kwargs = self.build_field(
+ field_name, info, model, depth
+ )
- if nested:
- field = self.get_nested_field(None, related_model, to_many)
- else:
- field = self.get_related_field(None, related_model, to_many)
-
- if field:
- if has_through_model:
- field.read_only = True
-
- ret[accessor_name] = field
-
- # Add the `read_only` flag to any fields that have bee specified
- # in the `read_only_fields` option
- for field_name in self.opts.read_only_fields:
- assert field_name not in self.base_fields.keys(), \
- "field '%s' on serializer '%s' specfied in " \
- "`read_only_fields`, but also added " \
- "as an explict field. Remove it from `read_only_fields`." % \
- (field_name, self.__class__.__name__)
- assert field_name in ret, \
- "Noexistant field '%s' specified in `read_only_fields` " \
- "on serializer '%s'." % \
- (field_name, self.__class__.__name__)
- ret[field_name].read_only = True
+ # Include any kwargs defined in `Meta.extra_kwargs`
+ extra_field_kwargs = extra_kwargs.get(field_name, {})
+ field_kwargs = self.include_extra_kwargs(
+ field_kwargs, extra_field_kwargs
+ )
- return ret
+ # Create the serializer field.
+ fields[field_name] = field_class(**field_kwargs)
- def get_pk_field(self, model_field):
+ # Add in any hidden fields.
+ fields.update(hidden_fields)
+
+ return fields
+
+ # Methods for determining the set of field names to include...
+
+ def get_field_names(self, declared_fields, info):
"""
- Returns a default instance of the pk field.
+ Returns the list of all field names that should be created when
+ instantiating this serializer class. This is based on the default
+ set of fields, but also takes into account the `Meta.fields` or
+ `Meta.exclude` options if they have been specified.
"""
- return self.get_field(model_field)
+ fields = getattr(self.Meta, 'fields', None)
+ exclude = getattr(self.Meta, 'exclude', None)
- def get_nested_field(self, model_field, related_model, to_many):
- """
- Creates a default instance of a nested relational field.
+ if fields and not isinstance(fields, (list, tuple)):
+ raise TypeError(
+ 'The `fields` option must be a list or tuple. Got %s.' %
+ type(fields).__name__
+ )
- Note that model_field will be `None` for reverse relationships.
- """
- class NestedModelSerializer(ModelSerializer):
- class Meta:
- model = related_model
- depth = self.opts.depth - 1
+ if exclude and not isinstance(exclude, (list, tuple)):
+ raise TypeError(
+ 'The `exclude` option must be a list or tuple. Got %s.' %
+ type(exclude).__name__
+ )
- return NestedModelSerializer(many=to_many)
+ assert not (fields and exclude), (
+ "Cannot set both 'fields' and 'exclude' options on "
+ "serializer {serializer_class}.".format(
+ serializer_class=self.__class__.__name__
+ )
+ )
+
+ if fields is not None:
+ # Ensure that all declared fields have also been included in the
+ # `Meta.fields` option.
+
+ # Do not require any fields that are declared a parent class,
+ # in order to allow serializer subclasses to only include
+ # a subset of fields.
+ required_field_names = set(declared_fields)
+ for cls in self.__class__.__bases__:
+ required_field_names -= set(getattr(cls, '_declared_fields', []))
+
+ for field_name in required_field_names:
+ assert field_name in fields, (
+ "The field '{field_name}' was declared on serializer "
+ "{serializer_class}, but has not been included in the "
+ "'fields' option.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ return fields
+
+ # Use the default set of field names if `Meta.fields` is not specified.
+ fields = self.get_default_field_names(declared_fields, info)
+
+ if exclude is not None:
+ # If `Meta.exclude` is included, then remove those fields.
+ for field_name in exclude:
+ assert field_name in fields, (
+ "The field '{field_name}' was include on serializer "
+ "{serializer_class} in the 'exclude' option, but does "
+ "not match any model field.".format(
+ field_name=field_name,
+ serializer_class=self.__class__.__name__
+ )
+ )
+ fields.remove(field_name)
- def get_related_field(self, model_field, related_model, to_many):
+ return fields
+
+ def get_default_field_names(self, declared_fields, model_info):
+ """
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
"""
- Creates a default instance of a flat relational field.
+ return (
+ [model_info.pk.name] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
+
+ # Methods for constructing serializer fields...
- Note that model_field will be `None` for reverse relationships.
+ def build_field(self, field_name, info, model_class, nested_depth):
"""
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
+ Return a two tuple of (cls, kwargs) to build a serializer field with.
+ """
+ if field_name in info.fields_and_pk:
+ model_field = info.fields_and_pk[field_name]
+ return self.build_standard_field(field_name, model_field)
+
+ elif field_name in info.relations:
+ relation_info = info.relations[field_name]
+ if not nested_depth:
+ return self.build_relational_field(field_name, relation_info)
+ else:
+ return self.build_nested_field(field_name, relation_info, nested_depth)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'many': to_many
- }
+ elif hasattr(model_class, field_name):
+ return self.build_property_field(field_name, model_class)
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
+ elif field_name == api_settings.URL_FIELD_NAME:
+ return self.build_url_field(field_name, model_class)
- return PrimaryKeyRelatedField(**kwargs)
+ return self.build_unknown_field(field_name, model_class)
- def get_field(self, model_field):
+ def build_standard_field(self, field_name, model_field):
"""
- Creates a default instance of a basic non-relational field.
+ Create regular model fields.
"""
- kwargs = {}
+ field_mapping = ClassLookupDict(self.serializer_field_mapping)
+
+ field_class = field_mapping[model_field]
+ field_kwargs = get_field_kwargs(field_name, model_field)
+
+ if 'choices' in field_kwargs:
+ # Fields with choices get coerced into `ChoiceField`
+ # instead of using their regular typed field.
+ field_class = self.serializer_choice_field
+
+ if not issubclass(field_class, ModelField):
+ # `model_field` is only valid for the fallback case of
+ # `ModelField`, which is used when no other typed field
+ # matched to the model field.
+ field_kwargs.pop('model_field', None)
+
+ if not issubclass(field_class, CharField) and not issubclass(field_class, ChoiceField):
+ # `allow_blank` is only valid for textual fields.
+ field_kwargs.pop('allow_blank', None)
+
+ if postgres_fields and isinstance(model_field, postgres_fields.ArrayField):
+ # Populate the `child` argument on `ListField` instances generated
+ # for the PostgrSQL specfic `ArrayField`.
+ child_model_field = model_field.base_field
+ child_field_class, child_field_kwargs = self.build_standard_field(
+ 'child', child_model_field
+ )
+ field_kwargs['child'] = child_field_class(**child_field_kwargs)
- if model_field.null or model_field.blank:
- kwargs['required'] = False
+ return field_class, field_kwargs
- if isinstance(model_field, models.AutoField) or not model_field.editable:
- kwargs['read_only'] = True
+ def build_relational_field(self, field_name, relation_info):
+ """
+ Create fields for forward and reverse relationships.
+ """
+ field_class = self.serializer_related_field
+ field_kwargs = get_relation_kwargs(field_name, relation_info)
- if model_field.has_default():
- kwargs['default'] = model_field.get_default()
+ # `view_name` is only valid for hyperlinked relationships.
+ if not issubclass(field_class, HyperlinkedRelatedField):
+ field_kwargs.pop('view_name', None)
- if issubclass(model_field.__class__, models.TextField):
- kwargs['widget'] = widgets.Textarea
+ return field_class, field_kwargs
- if model_field.verbose_name is not None:
- kwargs['label'] = model_field.verbose_name
+ def build_nested_field(self, field_name, relation_info, nested_depth):
+ """
+ Create nested fields for forward and reverse relationships.
+ """
+ class NestedSerializer(ModelSerializer):
+ class Meta:
+ model = relation_info.related_model
+ depth = nested_depth
- if model_field.help_text is not None:
- kwargs['help_text'] = model_field.help_text
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
- # TODO: TypedChoiceField?
- if model_field.flatchoices: # This ModelField contains choices
- kwargs['choices'] = model_field.flatchoices
- return ChoiceField(**kwargs)
+ return field_class, field_kwargs
- # put this below the ChoiceField because min_value isn't a valid initializer
- if issubclass(model_field.__class__, models.PositiveIntegerField) or\
- issubclass(model_field.__class__, models.PositiveSmallIntegerField):
- kwargs['min_value'] = 0
+ def build_property_field(self, field_name, model_class):
+ """
+ Create a read only field for model methods and properties.
+ """
+ field_class = ReadOnlyField
+ field_kwargs = {}
- attribute_dict = {
- models.CharField: ['max_length'],
- models.CommaSeparatedIntegerField: ['max_length'],
- models.DecimalField: ['max_digits', 'decimal_places'],
- models.EmailField: ['max_length'],
- models.FileField: ['max_length'],
- models.ImageField: ['max_length'],
- models.SlugField: ['max_length'],
- models.URLField: ['max_length'],
- }
+ return field_class, field_kwargs
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
- for attribute in attributes:
- kwargs.update({attribute: getattr(model_field, attribute)})
+ def build_url_field(self, field_name, model_class):
+ """
+ Create a field representing the object's own URL.
+ """
+ field_class = self.serializer_url_field
+ field_kwargs = get_url_kwargs(model_class)
- try:
- return self.field_mapping[model_field.__class__](**kwargs)
- except KeyError:
- return ModelField(model_field=model_field, **kwargs)
+ return field_class, field_kwargs
- def get_validation_exclusions(self):
+ def build_unknown_field(self, field_name, model_class):
"""
- Return a list of field names to exclude from model validation.
+ Raise an error on any unknown fields.
"""
- cls = self.opts.model
- opts = get_concrete_model(cls)._meta
- exclusions = [field.name for field in opts.fields + opts.many_to_many]
- for field_name, field in self.fields.items():
- field_name = field.source or field_name
- if field_name in exclusions and not field.read_only:
- exclusions.remove(field_name)
- return exclusions
+ raise ImproperlyConfigured(
+ 'Field name `%s` is not valid for model `%s`.' %
+ (field_name, model_class.__name__)
+ )
- def full_clean(self, instance):
+ def include_extra_kwargs(self, kwargs, extra_kwargs):
"""
- Perform Django's full_clean, and populate the `errors` dictionary
- if any validation errors occur.
+ Include any 'extra_kwargs' that have been included for this field,
+ possibly removing any incompatible existing keyword arguments.
+ """
+ if extra_kwargs.get('read_only', False):
+ for attr in [
+ 'required', 'default', 'allow_blank', 'allow_null',
+ 'min_length', 'max_length', 'min_value', 'max_value',
+ 'validators', 'queryset'
+ ]:
+ kwargs.pop(attr, None)
+
+ if extra_kwargs.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
- Note that we don't perform this inside the `.restore_object()` method,
- so that subclasses can override `.restore_object()`, and still get
- the full_clean validation checking.
+ kwargs.update(extra_kwargs)
+
+ return kwargs
+
+ # Methods for determining additional keyword arguments to apply...
+
+ def get_extra_kwargs(self):
"""
- try:
- instance.full_clean(exclude=self.get_validation_exclusions())
- except ValidationError as err:
- self._errors = err.message_dict
- return None
- return instance
+ Return a dictionary mapping field names to a dictionary of
+ additional keyword arguments.
+ """
+ extra_kwargs = getattr(self.Meta, 'extra_kwargs', {})
+
+ read_only_fields = getattr(self.Meta, 'read_only_fields', None)
+ if read_only_fields is not None:
+ for field_name in read_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['read_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ # These are all pending deprecation.
+ write_only_fields = getattr(self.Meta, 'write_only_fields', None)
+ if write_only_fields is not None:
+ warnings.warn(
+ "The `Meta.write_only_fields` option is deprecated. "
+ "Use `Meta.extra_kwargs={<field_name>: {'write_only': True}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ for field_name in write_only_fields:
+ kwargs = extra_kwargs.get(field_name, {})
+ kwargs['write_only'] = True
+ extra_kwargs[field_name] = kwargs
+
+ view_name = getattr(self.Meta, 'view_name', None)
+ if view_name is not None:
+ warnings.warn(
+ "The `Meta.view_name` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'view_name': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['view_name'] = view_name
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
+
+ lookup_field = getattr(self.Meta, 'lookup_field', None)
+ if lookup_field is not None:
+ warnings.warn(
+ "The `Meta.lookup_field` option is deprecated. "
+ "Use `Meta.extra_kwargs={'url': {'lookup_field': ...}}` instead.",
+ DeprecationWarning,
+ stacklevel=3
+ )
+ kwargs = extra_kwargs.get(api_settings.URL_FIELD_NAME, {})
+ kwargs['lookup_field'] = lookup_field
+ extra_kwargs[api_settings.URL_FIELD_NAME] = kwargs
- def restore_object(self, attrs, instance=None):
+ return extra_kwargs
+
+ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
"""
- Restore the model instance.
+ Return any additional field options that need to be included as a
+ result of uniqueness constraints on the model. This is returned as
+ a two-tuple of:
+
+ ('dict of updated extra kwargs', 'mapping of hidden fields')
"""
- m2m_data = {}
- related_data = {}
- meta = self.opts.model._meta
+ model = getattr(self.Meta, 'model')
+ model_fields = self._get_model_fields(
+ field_names, declared_fields, extra_kwargs
+ )
- # Reverse fk or one-to-one relations
- for (obj, model) in meta.get_all_related_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- related_data[field_name] = attrs.pop(field_name)
+ # Determine if we need any additional `HiddenField` or extra keyword
+ # arguments to deal with `unique_for` dates that are required to
+ # be in the input data in order to validate it.
+ unique_constraint_names = set()
+
+ for model_field in model_fields.values():
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
+ model_field.unique_for_date,
+ model_field.unique_for_month,
+ model_field.unique_for_year
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(field_names).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ hidden_fields = {}
+ uniqueness_extra_kwargs = {}
+
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is referred too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
- # Reverse m2m relations
- for (obj, model) in meta.get_all_related_m2m_objects_with_model():
- field_name = obj.field.related_query_name()
- if field_name in attrs:
- m2m_data[field_name] = attrs.pop(field_name)
+ if unique_constraint_name in model_fields:
+ # The corresponding field is present in the serializer
+ if default is empty:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'required': True}
+ else:
+ uniqueness_extra_kwargs[unique_constraint_name] = {'default': default}
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
+
+ # Update `extra_kwargs` with any new options.
+ for key, value in uniqueness_extra_kwargs.items():
+ if key in extra_kwargs:
+ extra_kwargs[key].update(value)
+ else:
+ extra_kwargs[key] = value
- # Forward m2m relations
- for field in meta.many_to_many:
- if field.name in attrs:
- m2m_data[field.name] = attrs.pop(field.name)
+ return extra_kwargs, hidden_fields
- # Update an existing instance...
- if instance is not None:
- for key, val in attrs.items():
- setattr(instance, key, val)
+ def _get_model_fields(self, field_names, declared_fields, extra_kwargs):
+ """
+ Returns all the model fields that are being mapped to by fields
+ on the serializer class.
+ Returned as a dict of 'model field name' -> 'model field'.
+ Used internally by `get_uniqueness_field_options`.
+ """
+ model = getattr(self.Meta, 'model')
+ model_fields = {}
+
+ for field_name in field_names:
+ if field_name in declared_fields:
+ # If the field is declared on the serializer
+ field = declared_fields[field_name]
+ source = field.source or field_name
+ else:
+ try:
+ source = extra_kwargs[field_name]['source']
+ except KeyError:
+ source = field_name
+
+ if '.' in source or source == '*':
+ # Model fields will always have a simple source mapping,
+ # they can't be nested attribute lookups.
+ continue
- # ...or create a new instance
- else:
- instance = self.opts.model(**attrs)
+ try:
+ field = model._meta.get_field(source)
+ if isinstance(field, DjangoModelField):
+ model_fields[source] = field
+ except FieldDoesNotExist:
+ pass
- # Any relations that cannot be set until we've
- # saved the model get hidden away on these
- # private attributes, so we can deal with them
- # at the point of save.
- instance._related_data = related_data
- instance._m2m_data = m2m_data
+ return model_fields
- return instance
+ # Determine the validators to apply...
- def from_native(self, data, files):
+ def get_validators(self):
"""
- Override the default method to also include model field validation.
+ Determine the set of validators to use when instantiating serializer.
"""
- instance = super(ModelSerializer, self).from_native(data, files)
- if not self._errors:
- return self.full_clean(instance)
+ # If the validators have been declared explicitly then use that.
+ validators = getattr(getattr(self, 'Meta', None), 'validators', None)
+ if validators is not None:
+ return validators[:]
+
+ # Otherwise use the default set of validators.
+ return (
+ self.get_unique_together_validators() +
+ self.get_unique_for_date_validators()
+ )
- def save_object(self, obj, **kwargs):
+ def get_unique_together_validators(self):
"""
- Save the deserialized object and return it.
+ Determine a default set of validators for any unique_together contraints.
"""
- obj.save(**kwargs)
+ model_class_inheritance_tree = (
+ [self.Meta.model] +
+ list(self.Meta.model._meta.parents.keys())
+ )
- if getattr(obj, '_m2m_data', None):
- for accessor_name, object_list in obj._m2m_data.items():
- setattr(obj, accessor_name, object_list)
- del(obj._m2m_data)
+ # The field names we're passing though here only include fields
+ # which may map onto a model field. Any dotted field name lookups
+ # cannot map to a field, and must be a traversal, so we're not
+ # including those.
+ field_names = set([
+ field.source for field in self.fields.values()
+ if (field.source != '*') and ('.' not in field.source)
+ ])
+
+ # Note that we make sure to check `unique_together` both on the
+ # base model class, but also on any parent classes.
+ validators = []
+ for parent_class in model_class_inheritance_tree:
+ for unique_together in parent_class._meta.unique_together:
+ if field_names.issuperset(set(unique_together)):
+ validator = UniqueTogetherValidator(
+ queryset=parent_class._default_manager,
+ fields=unique_together
+ )
+ validators.append(validator)
+ return validators
- if getattr(obj, '_related_data', None):
- for accessor_name, related in obj._related_data.items():
- setattr(obj, accessor_name, related)
- del(obj._related_data)
+ def get_unique_for_date_validators(self):
+ """
+ Determine a default set of validators for the following contraints:
+ * unique_for_date
+ * unique_for_month
+ * unique_for_year
+ """
+ info = model_meta.get_field_info(self.Meta.model)
+ default_manager = self.Meta.model._default_manager
+ field_names = [field.source for field in self.fields.values()]
-class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
- """
- Options for HyperlinkedModelSerializer
- """
- def __init__(self, meta):
- super(HyperlinkedModelSerializerOptions, self).__init__(meta)
- self.view_name = getattr(meta, 'view_name', None)
- self.lookup_field = getattr(meta, 'lookup_field', None)
+ validators = []
+ for field_name, field in info.fields_and_pk.items():
+ if field.unique_for_date and field_name in field_names:
+ validator = UniqueForDateValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_date
+ )
+ validators.append(validator)
-class HyperlinkedModelSerializer(ModelSerializer):
- """
- A subclass of ModelSerializer that uses hyperlinked relationships,
- instead of primary key relationships.
- """
- _options_class = HyperlinkedModelSerializerOptions
- _default_view_name = '%(model_name)s-detail'
- _hyperlink_field_class = HyperlinkedRelatedField
+ if field.unique_for_month and field_name in field_names:
+ validator = UniqueForMonthValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_month
+ )
+ validators.append(validator)
- def get_default_fields(self):
- fields = super(HyperlinkedModelSerializer, self).get_default_fields()
+ if field.unique_for_year and field_name in field_names:
+ validator = UniqueForYearValidator(
+ queryset=default_manager,
+ field=field_name,
+ date_field=field.unique_for_year
+ )
+ validators.append(validator)
- if self.opts.view_name is None:
- self.opts.view_name = self._get_default_view_name(self.opts.model)
+ return validators
- if 'url' not in fields:
- url_field = HyperlinkedIdentityField(
- view_name=self.opts.view_name,
- lookup_field=self.opts.lookup_field
- )
- ret = self._dict_class()
- ret['url'] = url_field
- ret.update(fields)
- fields = ret
- return fields
+if hasattr(models, 'UUIDField'):
+ ModelSerializer.serializer_field_mapping[models.UUIDField] = UUIDField
- def get_pk_field(self, model_field):
- if self.opts.fields and model_field.name in self.opts.fields:
- return self.get_field(model_field)
+if postgres_fields:
+ class CharMappingField(DictField):
+ child = CharField()
- def get_related_field(self, model_field, related_model, to_many):
- """
- Creates a default instance of a flat relational field.
- """
- # TODO: filter queryset using:
- # .using(db).complex_filter(self.rel.limit_choices_to)
- kwargs = {
- 'queryset': related_model._default_manager,
- 'view_name': self._get_default_view_name(related_model),
- 'many': to_many
- }
+ ModelSerializer.serializer_field_mapping[postgres_fields.HStoreField] = CharMappingField
+ ModelSerializer.serializer_field_mapping[postgres_fields.ArrayField] = ListField
- if model_field:
- kwargs['required'] = not(model_field.null or model_field.blank)
- if self.opts.lookup_field:
- kwargs['lookup_field'] = self.opts.lookup_field
+class HyperlinkedModelSerializer(ModelSerializer):
+ """
+ A type of `ModelSerializer` that uses hyperlinked relationships instead
+ of primary key relationships. Specifically:
- return self._hyperlink_field_class(**kwargs)
+ * A 'url' field is included instead of the 'id' field.
+ * Relationships to other instances are hyperlinks, instead of primary keys.
+ """
+ serializer_related_field = HyperlinkedRelatedField
- def get_identity(self, data):
+ def get_default_field_names(self, declared_fields, model_info):
"""
- This hook is required for bulk update.
- We need to override the default, to use the url as the identity.
+ Return the default list of field names that will be used if the
+ `Meta.fields` option is not specified.
"""
- try:
- return data.get('url', None)
- except AttributeError:
- return None
+ return (
+ [api_settings.URL_FIELD_NAME] +
+ list(declared_fields.keys()) +
+ list(model_info.fields.keys()) +
+ list(model_info.forward_relations.keys())
+ )
- def _get_default_view_name(self, model):
+ def build_nested_field(self, field_name, relation_info, nested_depth):
"""
- Return the view name to use if 'view_name' is not specified in 'Meta'
+ Create nested fields for forward and reverse relationships.
"""
- model_meta = model._meta
- format_kwargs = {
- 'app_label': model_meta.app_label,
- 'model_name': model_meta.object_name.lower()
- }
- return self._default_view_name % format_kwargs
+ class NestedSerializer(HyperlinkedModelSerializer):
+ class Meta:
+ model = relation_info.related_model
+ depth = nested_depth - 1
+
+ field_class = NestedSerializer
+ field_kwargs = get_nested_relation_kwargs(relation_info)
+
+ return field_class, field_kwargs
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 8fd177d5..a3e9f590 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -5,11 +5,11 @@ For example your project's `settings.py` file might look like this:
REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
- 'rest_framework.renderers.YAMLRenderer',
+ 'rest_framework.renderers.TemplateHTMLRenderer',
)
'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.JSONParser',
- 'rest_framework.parsers.YAMLParser',
+ 'rest_framework.parsers.TemplateHTMLRenderer',
)
}
@@ -18,13 +18,11 @@ REST framework settings, checking for user settings first, then falling
back to the defaults.
"""
from __future__ import unicode_literals
-
+from django.test.signals import setting_changed
from django.conf import settings
-from django.utils import importlib
-
+from django.utils import six
from rest_framework import ISO_8601
-from rest_framework.compat import six
-
+from rest_framework.compat import importlib
USER_SETTINGS = getattr(settings, 'REST_FRAMEWORK', None)
@@ -46,17 +44,13 @@ DEFAULTS = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
- 'DEFAULT_THROTTLE_CLASSES': (
- ),
-
- 'DEFAULT_CONTENT_NEGOTIATION_CLASS':
- 'rest_framework.negotiation.DefaultContentNegotiation',
+ 'DEFAULT_THROTTLE_CLASSES': (),
+ 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
+ 'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
+ 'DEFAULT_VERSIONING_CLASS': None,
- # Genric view behavior
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.ModelSerializer',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
- 'rest_framework.pagination.PaginationSerializer',
+ # Generic view behavior
+ 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_FILTER_BACKENDS': (),
# Throttling
@@ -64,15 +58,32 @@ DEFAULTS = {
'user': None,
'anon': None,
},
+ 'NUM_PROXIES': None,
# Pagination
- 'PAGINATE_BY': None,
- 'PAGINATE_BY_PARAM': None,
+ 'PAGE_SIZE': None,
+
+ # Filtering
+ 'SEARCH_PARAM': 'search',
+ 'ORDERING_PARAM': 'ordering',
+
+ # Versioning
+ 'DEFAULT_VERSION': None,
+ 'ALLOWED_VERSIONS': None,
+ 'VERSION_PARAM': 'version',
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None,
+ # View configuration
+ 'VIEW_NAME_FUNCTION': 'rest_framework.views.get_view_name',
+ 'VIEW_DESCRIPTION_FUNCTION': 'rest_framework.views.get_view_description',
+
+ # Exception handling
+ 'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler',
+ 'NON_FIELD_ERRORS_KEY': 'non_field_errors',
+
# Testing
'TEST_REQUEST_RENDERER_CLASSES': (
'rest_framework.renderers.MultiPartRenderer',
@@ -88,25 +99,28 @@ DEFAULTS = {
'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format',
+ 'URL_FIELD_NAME': 'url',
# Input and output formats
- 'DATE_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATE_FORMAT': None,
+ 'DATE_FORMAT': ISO_8601,
+ 'DATE_INPUT_FORMATS': (ISO_8601,),
- 'DATETIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'DATETIME_FORMAT': None,
+ 'DATETIME_FORMAT': ISO_8601,
+ 'DATETIME_INPUT_FORMATS': (ISO_8601,),
- 'TIME_INPUT_FORMATS': (
- ISO_8601,
- ),
- 'TIME_FORMAT': None,
+ 'TIME_FORMAT': ISO_8601,
+ 'TIME_INPUT_FORMATS': (ISO_8601,),
- # Pending deprecation
- 'FILTER_BACKEND': None,
+ # Encoding
+ 'UNICODE_JSON': True,
+ 'COMPACT_JSON': True,
+ 'COERCE_DECIMAL_TO_STRING': True,
+ 'UPLOADED_FILES_USE_URL': True,
+
+ # Pending deprecation:
+ 'PAGINATE_BY': None,
+ 'PAGINATE_BY_PARAM': None,
+ 'MAX_PAGINATE_BY': None
}
@@ -118,13 +132,16 @@ IMPORT_STRINGS = (
'DEFAULT_PERMISSION_CLASSES',
'DEFAULT_THROTTLE_CLASSES',
'DEFAULT_CONTENT_NEGOTIATION_CLASS',
- 'DEFAULT_MODEL_SERIALIZER_CLASS',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
+ 'DEFAULT_METADATA_CLASS',
+ 'DEFAULT_VERSIONING_CLASS',
+ 'DEFAULT_PAGINATION_CLASS',
'DEFAULT_FILTER_BACKENDS',
- 'FILTER_BACKEND',
+ 'EXCEPTION_HANDLER',
'TEST_REQUEST_RENDERER_CLASSES',
'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN',
+ 'VIEW_NAME_FUNCTION',
+ 'VIEW_DESCRIPTION_FUNCTION'
)
@@ -133,7 +150,9 @@ def perform_import(val, setting_name):
If the given setting is a string import notation,
then perform the necessary import or imports.
"""
- if isinstance(val, six.string_types):
+ if val is None:
+ return None
+ elif isinstance(val, six.string_types):
return import_from_string(val, setting_name)
elif isinstance(val, (list, tuple)):
return [import_from_string(item, setting_name) for item in val]
@@ -161,15 +180,15 @@ class APISettings(object):
For example:
from rest_framework.settings import api_settings
- print api_settings.DEFAULT_RENDERER_CLASSES
+ print(api_settings.DEFAULT_RENDERER_CLASSES)
Any setting with string import paths will be automatically resolved
and return the class, rather than the string literal.
"""
def __init__(self, user_settings=None, defaults=None, import_strings=None):
self.user_settings = user_settings or {}
- self.defaults = defaults or {}
- self.import_strings = import_strings or ()
+ self.defaults = defaults or DEFAULTS
+ self.import_strings = import_strings or IMPORT_STRINGS
def __getattr__(self, attr):
if attr not in self.defaults.keys():
@@ -186,15 +205,19 @@ class APISettings(object):
if val and attr in self.import_strings:
val = perform_import(val, attr)
- self.validate_setting(attr, val)
-
# Cache the result
setattr(self, attr, val)
return val
- def validate_setting(self, attr, val):
- if attr == 'FILTER_BACKEND' and val is not None:
- # Make sure we can initialize the class
- val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
+
+
+def reload_api_settings(*args, **kwargs):
+ global api_settings
+ setting, value = kwargs['setting'], kwargs['value']
+ if setting == 'REST_FRAMEWORK':
+ api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS)
+
+
+setting_changed.connect(reload_api_settings)
diff --git a/rest_framework/six.py b/rest_framework/six.py
deleted file mode 100644
index 9e382312..00000000
--- a/rest_framework/six.py
+++ /dev/null
@@ -1,389 +0,0 @@
-"""Utilities for writing code that runs on Python 2 and 3"""
-
-import operator
-import sys
-import types
-
-__author__ = "Benjamin Peterson <benjamin@python.org>"
-__version__ = "1.2.0"
-
-
-# True if we are running on Python 3.
-PY3 = sys.version_info[0] == 3
-
-if PY3:
- string_types = str,
- integer_types = int,
- class_types = type,
- text_type = str
- binary_type = bytes
-
- MAXSIZE = sys.maxsize
-else:
- string_types = basestring,
- integer_types = (int, long)
- class_types = (type, types.ClassType)
- text_type = unicode
- binary_type = str
-
- if sys.platform == "java":
- # Jython always uses 32 bits.
- MAXSIZE = int((1 << 31) - 1)
- else:
- # It's possible to have sizeof(long) != sizeof(Py_ssize_t).
- class X(object):
- def __len__(self):
- return 1 << 31
- try:
- len(X())
- except OverflowError:
- # 32-bit
- MAXSIZE = int((1 << 31) - 1)
- else:
- # 64-bit
- MAXSIZE = int((1 << 63) - 1)
- del X
-
-
-def _add_doc(func, doc):
- """Add documentation to a function."""
- func.__doc__ = doc
-
-
-def _import_module(name):
- """Import module, returning the module after the last dot."""
- __import__(name)
- return sys.modules[name]
-
-
-class _LazyDescr(object):
-
- def __init__(self, name):
- self.name = name
-
- def __get__(self, obj, tp):
- result = self._resolve()
- setattr(obj, self.name, result)
- # This is a bit ugly, but it avoids running this again.
- delattr(tp, self.name)
- return result
-
-
-class MovedModule(_LazyDescr):
-
- def __init__(self, name, old, new=None):
- super(MovedModule, self).__init__(name)
- if PY3:
- if new is None:
- new = name
- self.mod = new
- else:
- self.mod = old
-
- def _resolve(self):
- return _import_module(self.mod)
-
-
-class MovedAttribute(_LazyDescr):
-
- def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None):
- super(MovedAttribute, self).__init__(name)
- if PY3:
- if new_mod is None:
- new_mod = name
- self.mod = new_mod
- if new_attr is None:
- if old_attr is None:
- new_attr = name
- else:
- new_attr = old_attr
- self.attr = new_attr
- else:
- self.mod = old_mod
- if old_attr is None:
- old_attr = name
- self.attr = old_attr
-
- def _resolve(self):
- module = _import_module(self.mod)
- return getattr(module, self.attr)
-
-
-
-class _MovedItems(types.ModuleType):
- """Lazy loading of moved objects"""
-
-
-_moved_attributes = [
- MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"),
- MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"),
- MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"),
- MovedAttribute("map", "itertools", "builtins", "imap", "map"),
- MovedAttribute("reload_module", "__builtin__", "imp", "reload"),
- MovedAttribute("reduce", "__builtin__", "functools"),
- MovedAttribute("StringIO", "StringIO", "io"),
- MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"),
- MovedAttribute("zip", "itertools", "builtins", "izip", "zip"),
-
- MovedModule("builtins", "__builtin__"),
- MovedModule("configparser", "ConfigParser"),
- MovedModule("copyreg", "copy_reg"),
- MovedModule("http_cookiejar", "cookielib", "http.cookiejar"),
- MovedModule("http_cookies", "Cookie", "http.cookies"),
- MovedModule("html_entities", "htmlentitydefs", "html.entities"),
- MovedModule("html_parser", "HTMLParser", "html.parser"),
- MovedModule("http_client", "httplib", "http.client"),
- MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"),
- MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"),
- MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"),
- MovedModule("cPickle", "cPickle", "pickle"),
- MovedModule("queue", "Queue"),
- MovedModule("reprlib", "repr"),
- MovedModule("socketserver", "SocketServer"),
- MovedModule("tkinter", "Tkinter"),
- MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"),
- MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"),
- MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"),
- MovedModule("tkinter_tix", "Tix", "tkinter.tix"),
- MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"),
- MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"),
- MovedModule("tkinter_colorchooser", "tkColorChooser",
- "tkinter.colorchooser"),
- MovedModule("tkinter_commondialog", "tkCommonDialog",
- "tkinter.commondialog"),
- MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"),
- MovedModule("tkinter_font", "tkFont", "tkinter.font"),
- MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"),
- MovedModule("tkinter_tksimpledialog", "tkSimpleDialog",
- "tkinter.simpledialog"),
- MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"),
- MovedModule("winreg", "_winreg"),
-]
-for attr in _moved_attributes:
- setattr(_MovedItems, attr.name, attr)
-del attr
-
-moves = sys.modules["django.utils.six.moves"] = _MovedItems("moves")
-
-
-def add_move(move):
- """Add an item to six.moves."""
- setattr(_MovedItems, move.name, move)
-
-
-def remove_move(name):
- """Remove item from six.moves."""
- try:
- delattr(_MovedItems, name)
- except AttributeError:
- try:
- del moves.__dict__[name]
- except KeyError:
- raise AttributeError("no such move, %r" % (name,))
-
-
-if PY3:
- _meth_func = "__func__"
- _meth_self = "__self__"
-
- _func_code = "__code__"
- _func_defaults = "__defaults__"
-
- _iterkeys = "keys"
- _itervalues = "values"
- _iteritems = "items"
-else:
- _meth_func = "im_func"
- _meth_self = "im_self"
-
- _func_code = "func_code"
- _func_defaults = "func_defaults"
-
- _iterkeys = "iterkeys"
- _itervalues = "itervalues"
- _iteritems = "iteritems"
-
-
-try:
- advance_iterator = next
-except NameError:
- def advance_iterator(it):
- return it.next()
-next = advance_iterator
-
-
-if PY3:
- def get_unbound_function(unbound):
- return unbound
-
- Iterator = object
-
- def callable(obj):
- return any("__call__" in klass.__dict__ for klass in type(obj).__mro__)
-else:
- def get_unbound_function(unbound):
- return unbound.im_func
-
- class Iterator(object):
-
- def next(self):
- return type(self).__next__(self)
-
- callable = callable
-_add_doc(get_unbound_function,
- """Get the function out of a possibly unbound function""")
-
-
-get_method_function = operator.attrgetter(_meth_func)
-get_method_self = operator.attrgetter(_meth_self)
-get_function_code = operator.attrgetter(_func_code)
-get_function_defaults = operator.attrgetter(_func_defaults)
-
-
-def iterkeys(d):
- """Return an iterator over the keys of a dictionary."""
- return iter(getattr(d, _iterkeys)())
-
-def itervalues(d):
- """Return an iterator over the values of a dictionary."""
- return iter(getattr(d, _itervalues)())
-
-def iteritems(d):
- """Return an iterator over the (key, value) pairs of a dictionary."""
- return iter(getattr(d, _iteritems)())
-
-
-if PY3:
- def b(s):
- return s.encode("latin-1")
- def u(s):
- return s
- if sys.version_info[1] <= 1:
- def int2byte(i):
- return bytes((i,))
- else:
- # This is about 2x faster than the implementation above on 3.2+
- int2byte = operator.methodcaller("to_bytes", 1, "big")
- import io
- StringIO = io.StringIO
- BytesIO = io.BytesIO
-else:
- def b(s):
- return s
- def u(s):
- return unicode(s, "unicode_escape")
- int2byte = chr
- import StringIO
- StringIO = BytesIO = StringIO.StringIO
-_add_doc(b, """Byte literal""")
-_add_doc(u, """Text literal""")
-
-
-if PY3:
- import builtins
- exec_ = getattr(builtins, "exec")
-
-
- def reraise(tp, value, tb=None):
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
-
- print_ = getattr(builtins, "print")
- del builtins
-
-else:
- def exec_(code, globs=None, locs=None):
- """Execute code in a namespace."""
- if globs is None:
- frame = sys._getframe(1)
- globs = frame.f_globals
- if locs is None:
- locs = frame.f_locals
- del frame
- elif locs is None:
- locs = globs
- exec("""exec code in globs, locs""")
-
-
- exec_("""def reraise(tp, value, tb=None):
- raise tp, value, tb
-""")
-
-
- def print_(*args, **kwargs):
- """The new-style print function."""
- fp = kwargs.pop("file", sys.stdout)
- if fp is None:
- return
- def write(data):
- if not isinstance(data, basestring):
- data = str(data)
- fp.write(data)
- want_unicode = False
- sep = kwargs.pop("sep", None)
- if sep is not None:
- if isinstance(sep, unicode):
- want_unicode = True
- elif not isinstance(sep, str):
- raise TypeError("sep must be None or a string")
- end = kwargs.pop("end", None)
- if end is not None:
- if isinstance(end, unicode):
- want_unicode = True
- elif not isinstance(end, str):
- raise TypeError("end must be None or a string")
- if kwargs:
- raise TypeError("invalid keyword arguments to print()")
- if not want_unicode:
- for arg in args:
- if isinstance(arg, unicode):
- want_unicode = True
- break
- if want_unicode:
- newline = unicode("\n")
- space = unicode(" ")
- else:
- newline = "\n"
- space = " "
- if sep is None:
- sep = space
- if end is None:
- end = newline
- for i, arg in enumerate(args):
- if i:
- write(sep)
- write(arg)
- write(end)
-
-_add_doc(reraise, """Reraise an exception.""")
-
-
-def with_metaclass(meta, base=object):
- """Create a base class with a metaclass."""
- return meta("NewBase", (base,), {})
-
-
-### Additional customizations for Django ###
-
-if PY3:
- _iterlists = "lists"
- _assertRaisesRegex = "assertRaisesRegex"
-else:
- _iterlists = "iterlists"
- _assertRaisesRegex = "assertRaisesRegexp"
-
-
-def iterlists(d):
- """Return an iterator over the values of a MultiValueDict."""
- return getattr(d, _iterlists)()
-
-
-def assertRaisesRegex(self, *args, **kwargs):
- return getattr(self, _assertRaisesRegex)(*args, **kwargs)
-
-
-add_move(MovedModule("_dummy_thread", "dummy_thread"))
-add_move(MovedModule("_thread", "thread"))
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 6bfb778c..04f12ed3 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -6,30 +6,36 @@ a single block in the template.
*/
-
.form-actions {
- background: transparent;
- border-top-color: transparent;
- padding-top: 0;
+ background: transparent;
+ border-top-color: transparent;
+ padding-top: 0;
+ text-align: right;
+}
+
+#generic-content-form textarea {
+ font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace;
+ font-size: 80%;
}
.navbar-inverse .brand a {
- color: #999;
+ color: #999999;
}
.navbar-inverse .brand:hover a {
- color: white;
- text-decoration: none;
+ color: white;
+ text-decoration: none;
}
/* custom navigation styles */
-.wrapper .navbar{
+.navbar {
width: 100%;
- position: absolute;
+ position: fixed;
left: 0;
top: 0;
+ z-index: 3;
}
-.navbar .navbar-inner{
+.navbar {
background: #2C2C2C;
color: white;
border: none;
@@ -37,149 +43,165 @@ a single block in the template.
border-radius: 0px;
}
-.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
+.navbar .nav li, .navbar .nav li a, .navbar .brand:hover {
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
- background: #2c2c2c;
+ background: #2C2C2C;
+}
+
+.navbar .dropdown-menu li a, .navbar .dropdown-menu li {
+ color: #A30000;
}
-.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
- color: #A30000;
+.navbar .dropdown-menu li a:hover {
+ background: #EEEEEE;
+ color: #C20000;
}
-.navbar .navbar-inner .dropdown-menu li a:hover{
- background: #eeeeee;
- color: #c20000;
+
+.pagination>.disabled>a,
+.pagination>.disabled>a:hover,
+.pagination>.disabled>a:focus {
+ cursor: not-allowed;
+ pointer-events: none;
+}
+
+.pager>.disabled>a,
+.pager>.disabled>a:hover,
+.pager>.disabled>a:focus {
+ pointer-events: none;
+}
+
+.pager .next {
+ margin-left: 10px;
}
/*=== dabapps bootstrap styles ====*/
-html{
+html {
width:100%;
background: none;
}
-body, .navbar .navbar-inner .container-fluid {
+/*body, .navbar .container-fluid {
max-width: 1150px;
margin: 0 auto;
-}
+}*/
-body{
+body {
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
-#content{
- margin: 0;
+#content {
+ margin: 0;
+ padding-bottom: 60px;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
+
.wrapper {
+ position: relative;
+ top: 0;
+ left: 0;
+ padding-top: 60px;
+ margin: -60px 0;
min-height: 100%;
- height: auto !important;
- height: 100%;
- margin: 0 auto -60px;
}
.form-switcher {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.well {
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
}
.well .form-actions {
- padding-bottom: 0;
- margin-bottom: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
}
.well form {
- margin-bottom: 0;
-}
-
-.well form .help-block {
- color: #999;
+ margin-bottom: 0;
}
.nav-tabs {
- border: 0;
+ border: 0;
}
.nav-tabs > li {
- float: right;
+ float: right;
}
.nav-tabs li a {
- margin-right: 0;
+ margin-right: 0;
}
.nav-tabs > .active > a {
- background: #f5f5f5;
+ background: #F5F5F5;
}
.nav-tabs > .active > a:hover {
- background: #f5f5f5;
-}
-
-.tabbable.first-tab-active .tab-content
-{
- border-top-right-radius: 0;
+ background: #F5F5F5;
}
-#footer, #push {
- height: 60px; /* .push must be the same height as .footer */
+.tabbable.first-tab-active .tab-content {
+ border-top-right-radius: 0;
}
-#footer{
- text-align: right;
+footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ clear: both;
+ z-index: 10;
+ height: 60px;
+ width: 95%;
+ margin: 0 2.5%;
}
-#footer p {
+footer p {
text-align: center;
color: gray;
- border-top: 1px solid #DDD;
+ border-top: 1px solid #DDDDDD;
padding-top: 10px;
}
-#footer a {
- color: gray;
+footer a {
+ color: gray !important;
font-weight: bold;
}
-#footer a:hover {
+footer a:hover {
color: gray;
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
+ margin: 0;
}
/* custom general page styles */
-.hero-unit h2, .hero-unit h1{
+.hero-unit h1, .hero-unit h2 {
color: #A30000;
}
-body a, body a{
+body a {
color: #A30000;
}
-body a:hover{
+body a:hover {
color: #c20000;
}
-#content a span{
- text-decoration: underline;
- }
-
.request-info {
- clear:both;
+ clear:both;
}
diff --git a/rest_framework/static/rest_framework/css/bootstrap.min.css b/rest_framework/static/rest_framework/css/bootstrap.min.css
index 373f4b43..a9f35cee 100644
--- a/rest_framework/static/rest_framework/css/bootstrap.min.css
+++ b/rest_framework/static/rest_framework/css/bootstrap.min.css
@@ -1,841 +1,5 @@
/*!
- * Bootstrap v2.1.1
- *
- * Copyright 2012 Twitter, Inc
- * Licensed under the Apache License v2.0
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Designed and built with all the love in the world @twitter by @mdo and @fat.
- */
-.clearfix{*zoom:1;}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0;}
-.clearfix:after{clear:both;}
-.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0;}
-.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}
-article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
-audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
-audio:not([controls]){display:none;}
-html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
-a:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-a:hover,a:active{outline:0;}
-sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
-sup{top:-0.5em;}
-sub{bottom:-0.25em;}
-img{max-width:100%;width:auto\9;height:auto;vertical-align:middle;border:0;-ms-interpolation-mode:bicubic;}
-#map_canvas img{max-width:none;}
-button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
-button,input{*overflow:visible;line-height:normal;}
-button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
-button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
-input[type="search"]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield;}
-input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
-textarea{overflow:auto;vertical-align:top;}
-body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:20px;color:#333333;background-color:#ffffff;}
-a{color:#0088cc;text-decoration:none;}
-a:hover{color:#005580;text-decoration:underline;}
-.img-rounded{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.img-polaroid{padding:4px;background-color:#fff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.1);}
-.img-circle{-webkit-border-radius:500px;-moz-border-radius:500px;border-radius:500px;}
-.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;}
-.row:after{clear:both;}
-[class*="span"]{float:left;min-height:1px;margin-left:20px;}
-.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
-.span12{width:940px;}
-.span11{width:860px;}
-.span10{width:780px;}
-.span9{width:700px;}
-.span8{width:620px;}
-.span7{width:540px;}
-.span6{width:460px;}
-.span5{width:380px;}
-.span4{width:300px;}
-.span3{width:220px;}
-.span2{width:140px;}
-.span1{width:60px;}
-.offset12{margin-left:980px;}
-.offset11{margin-left:900px;}
-.offset10{margin-left:820px;}
-.offset9{margin-left:740px;}
-.offset8{margin-left:660px;}
-.offset7{margin-left:580px;}
-.offset6{margin-left:500px;}
-.offset5{margin-left:420px;}
-.offset4{margin-left:340px;}
-.offset3{margin-left:260px;}
-.offset2{margin-left:180px;}
-.offset1{margin-left:100px;}
-.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;}
-.row-fluid:after{clear:both;}
-.row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.127659574468085%;*margin-left:2.074468085106383%;}
-.row-fluid [class*="span"]:first-child{margin-left:0;}
-.row-fluid .span12{width:100%;*width:99.94680851063829%;}
-.row-fluid .span11{width:91.48936170212765%;*width:91.43617021276594%;}
-.row-fluid .span10{width:82.97872340425532%;*width:82.92553191489361%;}
-.row-fluid .span9{width:74.46808510638297%;*width:74.41489361702126%;}
-.row-fluid .span8{width:65.95744680851064%;*width:65.90425531914893%;}
-.row-fluid .span7{width:57.44680851063829%;*width:57.39361702127659%;}
-.row-fluid .span6{width:48.93617021276595%;*width:48.88297872340425%;}
-.row-fluid .span5{width:40.42553191489362%;*width:40.37234042553192%;}
-.row-fluid .span4{width:31.914893617021278%;*width:31.861702127659576%;}
-.row-fluid .span3{width:23.404255319148934%;*width:23.351063829787233%;}
-.row-fluid .span2{width:14.893617021276595%;*width:14.840425531914894%;}
-.row-fluid .span1{width:6.382978723404255%;*width:6.329787234042553%;}
-.row-fluid .offset12{margin-left:104.25531914893617%;*margin-left:104.14893617021275%;}
-.row-fluid .offset12:first-child{margin-left:102.12765957446808%;*margin-left:102.02127659574467%;}
-.row-fluid .offset11{margin-left:95.74468085106382%;*margin-left:95.6382978723404%;}
-.row-fluid .offset11:first-child{margin-left:93.61702127659574%;*margin-left:93.51063829787232%;}
-.row-fluid .offset10{margin-left:87.23404255319149%;*margin-left:87.12765957446807%;}
-.row-fluid .offset10:first-child{margin-left:85.1063829787234%;*margin-left:84.99999999999999%;}
-.row-fluid .offset9{margin-left:78.72340425531914%;*margin-left:78.61702127659572%;}
-.row-fluid .offset9:first-child{margin-left:76.59574468085106%;*margin-left:76.48936170212764%;}
-.row-fluid .offset8{margin-left:70.2127659574468%;*margin-left:70.10638297872339%;}
-.row-fluid .offset8:first-child{margin-left:68.08510638297872%;*margin-left:67.9787234042553%;}
-.row-fluid .offset7{margin-left:61.70212765957446%;*margin-left:61.59574468085106%;}
-.row-fluid .offset7:first-child{margin-left:59.574468085106375%;*margin-left:59.46808510638297%;}
-.row-fluid .offset6{margin-left:53.191489361702125%;*margin-left:53.085106382978715%;}
-.row-fluid .offset6:first-child{margin-left:51.063829787234035%;*margin-left:50.95744680851063%;}
-.row-fluid .offset5{margin-left:44.68085106382979%;*margin-left:44.57446808510638%;}
-.row-fluid .offset5:first-child{margin-left:42.5531914893617%;*margin-left:42.4468085106383%;}
-.row-fluid .offset4{margin-left:36.170212765957444%;*margin-left:36.06382978723405%;}
-.row-fluid .offset4:first-child{margin-left:34.04255319148936%;*margin-left:33.93617021276596%;}
-.row-fluid .offset3{margin-left:27.659574468085104%;*margin-left:27.5531914893617%;}
-.row-fluid .offset3:first-child{margin-left:25.53191489361702%;*margin-left:25.425531914893618%;}
-.row-fluid .offset2{margin-left:19.148936170212764%;*margin-left:19.04255319148936%;}
-.row-fluid .offset2:first-child{margin-left:17.02127659574468%;*margin-left:16.914893617021278%;}
-.row-fluid .offset1{margin-left:10.638297872340425%;*margin-left:10.53191489361702%;}
-.row-fluid .offset1:first-child{margin-left:8.51063829787234%;*margin-left:8.404255319148938%;}
-[class*="span"].hide,.row-fluid [class*="span"].hide{display:none;}
-[class*="span"].pull-right,.row-fluid [class*="span"].pull-right{float:right;}
-.container{margin-right:auto;margin-left:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";line-height:0;}
-.container:after{clear:both;}
-.container-fluid{padding-right:20px;padding-left:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";line-height:0;}
-.container-fluid:after{clear:both;}
-p{margin:0 0 10px;}
-.lead{margin-bottom:20px;font-size:21px;font-weight:200;line-height:30px;}
-small{font-size:85%;}
-strong{font-weight:bold;}
-em{font-style:italic;}
-cite{font-style:normal;}
-.muted{color:#999999;}
-.text-warning{color:#c09853;}
-.text-error{color:#b94a48;}
-.text-info{color:#3a87ad;}
-.text-success{color:#468847;}
-h1,h2,h3,h4,h5,h6{margin:10px 0;font-family:inherit;font-weight:bold;line-height:1;color:inherit;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;line-height:1;color:#999999;}
-h1{font-size:36px;line-height:40px;}
-h2{font-size:30px;line-height:40px;}
-h3{font-size:24px;line-height:40px;}
-h4{font-size:18px;line-height:20px;}
-h5{font-size:14px;line-height:20px;}
-h6{font-size:12px;line-height:20px;}
-h1 small{font-size:24px;}
-h2 small{font-size:18px;}
-h3 small{font-size:14px;}
-h4 small{font-size:14px;}
-.page-header{padding-bottom:9px;margin:20px 0 30px;border-bottom:1px solid #eeeeee;}
-ul,ol{padding:0;margin:0 0 10px 25px;}
-ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
-li{line-height:20px;}
-ul.unstyled,ol.unstyled{margin-left:0;list-style:none;}
-dl{margin-bottom:20px;}
-dt,dd{line-height:20px;}
-dt{font-weight:bold;}
-dd{margin-left:10px;}
-.dl-horizontal{*zoom:1;}.dl-horizontal:before,.dl-horizontal:after{display:table;content:"";line-height:0;}
-.dl-horizontal:after{clear:both;}
-.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
-.dl-horizontal dd{margin-left:180px;}
-hr{margin:20px 0;border:0;border-top:1px solid #eeeeee;border-bottom:1px solid #ffffff;}
-abbr[title]{cursor:help;border-bottom:1px dotted #999999;}
-abbr.initialism{font-size:90%;text-transform:uppercase;}
-blockquote{padding:0 0 0 15px;margin:0 0 20px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:25px;}
-blockquote small{display:block;line-height:20px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
-blockquote.pull-right{float:right;padding-right:15px;padding-left:0;border-right:5px solid #eeeeee;border-left:0;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
-blockquote.pull-right small:before{content:'';}
-blockquote.pull-right small:after{content:'\00A0 \2014';}
-q:before,q:after,blockquote:before,blockquote:after{content:"";}
-address{display:block;margin-bottom:20px;font-style:normal;line-height:20px;}
-code,pre{padding:0 3px 2px;font-family:Monaco,Menlo,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-code{padding:2px 4px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
-pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:20px;word-break:break-all;word-wrap:break-word;white-space:pre;white-space:pre-wrap;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}pre.prettyprint{margin-bottom:20px;}
-pre code{padding:0;color:inherit;background-color:transparent;border:0;}
-.pre-scrollable{max-height:340px;overflow-y:scroll;}
-.label,.badge{font-size:11.844px;font-weight:bold;line-height:14px;color:#ffffff;vertical-align:baseline;white-space:nowrap;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#999999;}
-.label{padding:1px 4px 2px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-.badge{padding:1px 9px 2px;-webkit-border-radius:9px;-moz-border-radius:9px;border-radius:9px;}
-a.label:hover,a.badge:hover{color:#ffffff;text-decoration:none;cursor:pointer;}
-.label-important,.badge-important{background-color:#b94a48;}
-.label-important[href],.badge-important[href]{background-color:#953b39;}
-.label-warning,.badge-warning{background-color:#f89406;}
-.label-warning[href],.badge-warning[href]{background-color:#c67605;}
-.label-success,.badge-success{background-color:#468847;}
-.label-success[href],.badge-success[href]{background-color:#356635;}
-.label-info,.badge-info{background-color:#3a87ad;}
-.label-info[href],.badge-info[href]{background-color:#2d6987;}
-.label-inverse,.badge-inverse{background-color:#333333;}
-.label-inverse[href],.badge-inverse[href]{background-color:#1a1a1a;}
-.btn .label,.btn .badge{position:relative;top:-1px;}
-.btn-mini .label,.btn-mini .badge{top:0;}
-table{max-width:100%;background-color:transparent;border-collapse:collapse;border-spacing:0;}
-.table{width:100%;margin-bottom:20px;}.table th,.table td{padding:8px;line-height:20px;text-align:left;vertical-align:top;border-top:1px solid #dddddd;}
-.table th{font-weight:bold;}
-.table thead th{vertical-align:bottom;}
-.table caption+thead tr:first-child th,.table caption+thead tr:first-child td,.table colgroup+thead tr:first-child th,.table colgroup+thead tr:first-child td,.table thead:first-child tr:first-child th,.table thead:first-child tr:first-child td{border-top:0;}
-.table tbody+tbody{border-top:2px solid #dddddd;}
-.table-condensed th,.table-condensed td{padding:4px 5px;}
-.table-bordered{border:1px solid #dddddd;border-collapse:separate;*border-collapse:collapse;border-left:0;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th,.table-bordered td{border-left:1px solid #dddddd;}
-.table-bordered caption+thead tr:first-child th,.table-bordered caption+tbody tr:first-child th,.table-bordered caption+tbody tr:first-child td,.table-bordered colgroup+thead tr:first-child th,.table-bordered colgroup+tbody tr:first-child th,.table-bordered colgroup+tbody tr:first-child td,.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
-.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;}
-.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topright:4px;}
-.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child,.table-bordered tfoot:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;-webkit-border-bottom-left-radius:4px;border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;}
-.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child,.table-bordered tfoot:last-child tr:last-child td:last-child{-webkit-border-bottom-right-radius:4px;border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;}
-.table-bordered caption+thead tr:first-child th:first-child,.table-bordered caption+tbody tr:first-child td:first-child,.table-bordered colgroup+thead tr:first-child th:first-child,.table-bordered colgroup+tbody tr:first-child td:first-child{-webkit-border-top-left-radius:4px;border-top-left-radius:4px;-moz-border-radius-topleft:4px;}
-.table-bordered caption+thead tr:first-child th:last-child,.table-bordered caption+tbody tr:first-child td:last-child,.table-bordered colgroup+thead tr:first-child th:last-child,.table-bordered colgroup+tbody tr:first-child td:last-child{-webkit-border-top-right-radius:4px;border-top-right-radius:4px;-moz-border-radius-topleft:4px;}
-.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
-.table-hover tbody tr:hover td,.table-hover tbody tr:hover th{background-color:#f5f5f5;}
-table [class*=span],.row-fluid table [class*=span]{display:table-cell;float:none;margin-left:0;}
-.table .span1{float:none;width:44px;margin-left:0;}
-.table .span2{float:none;width:124px;margin-left:0;}
-.table .span3{float:none;width:204px;margin-left:0;}
-.table .span4{float:none;width:284px;margin-left:0;}
-.table .span5{float:none;width:364px;margin-left:0;}
-.table .span6{float:none;width:444px;margin-left:0;}
-.table .span7{float:none;width:524px;margin-left:0;}
-.table .span8{float:none;width:604px;margin-left:0;}
-.table .span9{float:none;width:684px;margin-left:0;}
-.table .span10{float:none;width:764px;margin-left:0;}
-.table .span11{float:none;width:844px;margin-left:0;}
-.table .span12{float:none;width:924px;margin-left:0;}
-.table .span13{float:none;width:1004px;margin-left:0;}
-.table .span14{float:none;width:1084px;margin-left:0;}
-.table .span15{float:none;width:1164px;margin-left:0;}
-.table .span16{float:none;width:1244px;margin-left:0;}
-.table .span17{float:none;width:1324px;margin-left:0;}
-.table .span18{float:none;width:1404px;margin-left:0;}
-.table .span19{float:none;width:1484px;margin-left:0;}
-.table .span20{float:none;width:1564px;margin-left:0;}
-.table .span21{float:none;width:1644px;margin-left:0;}
-.table .span22{float:none;width:1724px;margin-left:0;}
-.table .span23{float:none;width:1804px;margin-left:0;}
-.table .span24{float:none;width:1884px;margin-left:0;}
-.table tbody tr.success td{background-color:#dff0d8;}
-.table tbody tr.error td{background-color:#f2dede;}
-.table tbody tr.warning td{background-color:#fcf8e3;}
-.table tbody tr.info td{background-color:#d9edf7;}
-.table-hover tbody tr.success:hover td{background-color:#d0e9c6;}
-.table-hover tbody tr.error:hover td{background-color:#ebcccc;}
-.table-hover tbody tr.warning:hover td{background-color:#faf2cc;}
-.table-hover tbody tr.info:hover td{background-color:#c4e3f3;}
-form{margin:0 0 20px;}
-fieldset{padding:0;margin:0;border:0;}
-legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:40px;color:#333333;border:0;border-bottom:1px solid #e5e5e5;}legend small{font-size:15px;color:#999999;}
-label,input,button,select,textarea{font-size:14px;font-weight:normal;line-height:20px;}
-input,button,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;}
-label{display:block;margin-bottom:5px;}
-select,textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{display:inline-block;height:20px;padding:4px 6px;margin-bottom:9px;font-size:14px;line-height:20px;color:#555555;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-input,textarea,.uneditable-input{width:206px;}
-textarea{height:auto;}
-textarea,input[type="text"],input[type="password"],input[type="datetime"],input[type="datetime-local"],input[type="date"],input[type="month"],input[type="time"],input[type="week"],input[type="number"],input[type="email"],input[type="url"],input[type="search"],input[type="tel"],input[type="color"],.uneditable-input{background-color:#ffffff;border:1px solid #cccccc;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-webkit-transition:border linear .2s, box-shadow linear .2s;-moz-transition:border linear .2s, box-shadow linear .2s;-o-transition:border linear .2s, box-shadow linear .2s;transition:border linear .2s, box-shadow linear .2s;}textarea:focus,input[type="text"]:focus,input[type="password"]:focus,input[type="datetime"]:focus,input[type="datetime-local"]:focus,input[type="date"]:focus,input[type="month"]:focus,input[type="time"]:focus,input[type="week"]:focus,input[type="number"]:focus,input[type="email"]:focus,input[type="url"]:focus,input[type="search"]:focus,input[type="tel"]:focus,input[type="color"]:focus,.uneditable-input:focus{border-color:rgba(82, 168, 236, 0.8);outline:0;outline:thin dotted \9;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);}
-input[type="radio"],input[type="checkbox"]{margin:4px 0 0;*margin-top:0;margin-top:1px \9;line-height:normal;cursor:pointer;}
-input[type="file"],input[type="image"],input[type="submit"],input[type="reset"],input[type="button"],input[type="radio"],input[type="checkbox"]{width:auto;}
-select,input[type="file"]{height:30px;*margin-top:4px;line-height:30px;}
-select{width:220px;border:1px solid #cccccc;background-color:#ffffff;}
-select[multiple],select[size]{height:auto;}
-select:focus,input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-.uneditable-input,.uneditable-textarea{color:#999999;background-color:#fcfcfc;border-color:#cccccc;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
-.uneditable-input{overflow:hidden;white-space:nowrap;}
-.uneditable-textarea{width:auto;height:auto;}
-input:-moz-placeholder,textarea:-moz-placeholder{color:#999999;}
-input:-ms-input-placeholder,textarea:-ms-input-placeholder{color:#999999;}
-input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#999999;}
-.radio,.checkbox{min-height:18px;padding-left:18px;}
-.radio input[type="radio"],.checkbox input[type="checkbox"]{float:left;margin-left:-18px;}
-.controls>.radio:first-child,.controls>.checkbox:first-child{padding-top:5px;}
-.radio.inline,.checkbox.inline{display:inline-block;padding-top:5px;margin-bottom:0;vertical-align:middle;}
-.radio.inline+.radio.inline,.checkbox.inline+.checkbox.inline{margin-left:10px;}
-.input-mini{width:60px;}
-.input-small{width:90px;}
-.input-medium{width:150px;}
-.input-large{width:210px;}
-.input-xlarge{width:270px;}
-.input-xxlarge{width:530px;}
-input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"]{float:none;margin-left:0;}
-.input-append input[class*="span"],.input-append .uneditable-input[class*="span"],.input-prepend input[class*="span"],.input-prepend .uneditable-input[class*="span"],.row-fluid input[class*="span"],.row-fluid select[class*="span"],.row-fluid textarea[class*="span"],.row-fluid .uneditable-input[class*="span"],.row-fluid .input-prepend [class*="span"],.row-fluid .input-append [class*="span"]{display:inline-block;}
-input,textarea,.uneditable-input{margin-left:0;}
-.controls-row [class*="span"]+[class*="span"]{margin-left:20px;}
-input.span12, textarea.span12, .uneditable-input.span12{width:926px;}
-input.span11, textarea.span11, .uneditable-input.span11{width:846px;}
-input.span10, textarea.span10, .uneditable-input.span10{width:766px;}
-input.span9, textarea.span9, .uneditable-input.span9{width:686px;}
-input.span8, textarea.span8, .uneditable-input.span8{width:606px;}
-input.span7, textarea.span7, .uneditable-input.span7{width:526px;}
-input.span6, textarea.span6, .uneditable-input.span6{width:446px;}
-input.span5, textarea.span5, .uneditable-input.span5{width:366px;}
-input.span4, textarea.span4, .uneditable-input.span4{width:286px;}
-input.span3, textarea.span3, .uneditable-input.span3{width:206px;}
-input.span2, textarea.span2, .uneditable-input.span2{width:126px;}
-input.span1, textarea.span1, .uneditable-input.span1{width:46px;}
-.controls-row{*zoom:1;}.controls-row:before,.controls-row:after{display:table;content:"";line-height:0;}
-.controls-row:after{clear:both;}
-.controls-row [class*="span"]{float:left;}
-input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{cursor:not-allowed;background-color:#eeeeee;}
-input[type="radio"][disabled],input[type="checkbox"][disabled],input[type="radio"][readonly],input[type="checkbox"][readonly]{background-color:transparent;}
-.control-group.warning>label,.control-group.warning .help-block,.control-group.warning .help-inline{color:#c09853;}
-.control-group.warning .checkbox,.control-group.warning .radio,.control-group.warning input,.control-group.warning select,.control-group.warning textarea{color:#c09853;}
-.control-group.warning input,.control-group.warning select,.control-group.warning textarea{border-color:#c09853;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.warning input:focus,.control-group.warning select:focus,.control-group.warning textarea:focus{border-color:#a47e3c;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e;}
-.control-group.warning .input-prepend .add-on,.control-group.warning .input-append .add-on{color:#c09853;background-color:#fcf8e3;border-color:#c09853;}
-.control-group.error>label,.control-group.error .help-block,.control-group.error .help-inline{color:#b94a48;}
-.control-group.error .checkbox,.control-group.error .radio,.control-group.error input,.control-group.error select,.control-group.error textarea{color:#b94a48;}
-.control-group.error input,.control-group.error select,.control-group.error textarea{border-color:#b94a48;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.error input:focus,.control-group.error select:focus,.control-group.error textarea:focus{border-color:#953b39;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392;}
-.control-group.error .input-prepend .add-on,.control-group.error .input-append .add-on{color:#b94a48;background-color:#f2dede;border-color:#b94a48;}
-.control-group.success>label,.control-group.success .help-block,.control-group.success .help-inline{color:#468847;}
-.control-group.success .checkbox,.control-group.success .radio,.control-group.success input,.control-group.success select,.control-group.success textarea{color:#468847;}
-.control-group.success input,.control-group.success select,.control-group.success textarea{border-color:#468847;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.success input:focus,.control-group.success select:focus,.control-group.success textarea:focus{border-color:#356635;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b;}
-.control-group.success .input-prepend .add-on,.control-group.success .input-append .add-on{color:#468847;background-color:#dff0d8;border-color:#468847;}
-.control-group.info>label,.control-group.info .help-block,.control-group.info .help-inline{color:#3a87ad;}
-.control-group.info .checkbox,.control-group.info .radio,.control-group.info input,.control-group.info select,.control-group.info textarea{color:#3a87ad;}
-.control-group.info input,.control-group.info select,.control-group.info textarea{border-color:#3a87ad;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075);}.control-group.info input:focus,.control-group.info select:focus,.control-group.info textarea:focus{border-color:#2d6987;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7ab5d3;}
-.control-group.info .input-prepend .add-on,.control-group.info .input-append .add-on{color:#3a87ad;background-color:#d9edf7;border-color:#3a87ad;}
-input:focus:required:invalid,textarea:focus:required:invalid,select:focus:required:invalid{color:#b94a48;border-color:#ee5f5b;}input:focus:required:invalid:focus,textarea:focus:required:invalid:focus,select:focus:required:invalid:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
-.form-actions{padding:19px 20px 20px;margin-top:20px;margin-bottom:20px;background-color:#f5f5f5;border-top:1px solid #e5e5e5;*zoom:1;}.form-actions:before,.form-actions:after{display:table;content:"";line-height:0;}
-.form-actions:after{clear:both;}
-.help-block,.help-inline{color:#595959;}
-.help-block{display:block;margin-bottom:10px;}
-.help-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle;padding-left:5px;}
-.input-append,.input-prepend{margin-bottom:5px;font-size:0;white-space:nowrap;}.input-append input,.input-prepend input,.input-append select,.input-prepend select,.input-append .uneditable-input,.input-prepend .uneditable-input{position:relative;margin-bottom:0;*margin-left:0;font-size:14px;vertical-align:top;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}.input-append input:focus,.input-prepend input:focus,.input-append select:focus,.input-prepend select:focus,.input-append .uneditable-input:focus,.input-prepend .uneditable-input:focus{z-index:2;}
-.input-append .add-on,.input-prepend .add-on{display:inline-block;width:auto;height:20px;min-width:16px;padding:4px 5px;font-size:14px;font-weight:normal;line-height:20px;text-align:center;text-shadow:0 1px 0 #ffffff;background-color:#eeeeee;border:1px solid #ccc;}
-.input-append .add-on,.input-prepend .add-on,.input-append .btn,.input-prepend .btn{vertical-align:top;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.input-append .active,.input-prepend .active{background-color:#a9dba9;border-color:#46a546;}
-.input-prepend .add-on,.input-prepend .btn{margin-right:-1px;}
-.input-prepend .add-on:first-child,.input-prepend .btn:first-child{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-append input,.input-append select,.input-append .uneditable-input{-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-append .add-on,.input-append .btn{margin-left:-1px;}
-.input-append .add-on:last-child,.input-append .btn:last-child{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-.input-prepend.input-append input,.input-prepend.input-append select,.input-prepend.input-append .uneditable-input{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.input-prepend.input-append .add-on:first-child,.input-prepend.input-append .btn:first-child{margin-right:-1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.input-prepend.input-append .add-on:last-child,.input-prepend.input-append .btn:last-child{margin-left:-1px;-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-input.search-query{padding-right:14px;padding-right:4px \9;padding-left:14px;padding-left:4px \9;margin-bottom:0;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.form-search .input-append .search-query,.form-search .input-prepend .search-query{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.form-search .input-append .search-query{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;}
-.form-search .input-append .btn{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;}
-.form-search .input-prepend .search-query{-webkit-border-radius:0 14px 14px 0;-moz-border-radius:0 14px 14px 0;border-radius:0 14px 14px 0;}
-.form-search .input-prepend .btn{-webkit-border-radius:14px 0 0 14px;-moz-border-radius:14px 0 0 14px;border-radius:14px 0 0 14px;}
-.form-search input,.form-inline input,.form-horizontal input,.form-search textarea,.form-inline textarea,.form-horizontal textarea,.form-search select,.form-inline select,.form-horizontal select,.form-search .help-inline,.form-inline .help-inline,.form-horizontal .help-inline,.form-search .uneditable-input,.form-inline .uneditable-input,.form-horizontal .uneditable-input,.form-search .input-prepend,.form-inline .input-prepend,.form-horizontal .input-prepend,.form-search .input-append,.form-inline .input-append,.form-horizontal .input-append{display:inline-block;*display:inline;*zoom:1;margin-bottom:0;vertical-align:middle;}
-.form-search .hide,.form-inline .hide,.form-horizontal .hide{display:none;}
-.form-search label,.form-inline label,.form-search .btn-group,.form-inline .btn-group{display:inline-block;}
-.form-search .input-append,.form-inline .input-append,.form-search .input-prepend,.form-inline .input-prepend{margin-bottom:0;}
-.form-search .radio,.form-search .checkbox,.form-inline .radio,.form-inline .checkbox{padding-left:0;margin-bottom:0;vertical-align:middle;}
-.form-search .radio input[type="radio"],.form-search .checkbox input[type="checkbox"],.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:left;margin-right:3px;margin-left:0;}
-.control-group{margin-bottom:10px;}
-legend+.control-group{margin-top:20px;-webkit-margin-top-collapse:separate;}
-.form-horizontal .control-group{margin-bottom:20px;*zoom:1;}.form-horizontal .control-group:before,.form-horizontal .control-group:after{display:table;content:"";line-height:0;}
-.form-horizontal .control-group:after{clear:both;}
-.form-horizontal .control-label{float:left;width:160px;padding-top:5px;text-align:right;}
-.form-horizontal .controls{*display:inline-block;*padding-left:20px;margin-left:180px;*margin-left:0;}.form-horizontal .controls:first-child{*padding-left:180px;}
-.form-horizontal .help-block{margin-bottom:0;}
-.form-horizontal input+.help-block,.form-horizontal select+.help-block,.form-horizontal textarea+.help-block{margin-top:10px;}
-.form-horizontal .form-actions{padding-left:180px;}
-.btn{display:inline-block;*display:inline;*zoom:1;padding:4px 14px;margin-bottom:0;font-size:14px;line-height:20px;*line-height:20px;text-align:center;vertical-align:middle;cursor:pointer;color:#333333;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);background-color:#f5f5f5;background-image:-moz-linear-gradient(top, #ffffff, #e6e6e6);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(top, #ffffff, #e6e6e6);background-image:-o-linear-gradient(top, #ffffff, #e6e6e6);background-image:linear-gradient(to bottom, #ffffff, #e6e6e6);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe6e6e6', GradientType=0);border-color:#e6e6e6 #e6e6e6 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e6e6e6;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);border:1px solid #bbbbbb;*border:0;border-bottom-color:#a2a2a2;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;*margin-left:.3em;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);}.btn:hover,.btn:active,.btn.active,.btn.disabled,.btn[disabled]{color:#333333;background-color:#e6e6e6;*background-color:#d9d9d9;}
-.btn:active,.btn.active{background-color:#cccccc \9;}
-.btn:first-child{*margin-left:0;}
-.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;*background-color:#d9d9d9;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
-.btn:focus{outline:thin dotted #333;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
-.btn.active,.btn:active{background-color:#e6e6e6;background-color:#d9d9d9 \9;background-image:none;outline:0;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);}
-.btn.disabled,.btn[disabled]{cursor:default;background-color:#e6e6e6;background-image:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
-.btn-large{padding:9px 14px;font-size:16px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
-.btn-large [class^="icon-"]{margin-top:2px;}
-.btn-small{padding:3px 9px;font-size:12px;line-height:18px;}
-.btn-small [class^="icon-"]{margin-top:0;}
-.btn-mini{padding:2px 6px;font-size:11px;line-height:17px;}
-.btn-block{display:block;width:100%;padding-left:0;padding-right:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;}
-.btn-block+.btn-block{margin-top:5px;}
-input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%;}
-.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active,.btn-inverse.active{color:rgba(255, 255, 255, 0.75);}
-.btn{border-color:#c5c5c5;border-color:rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.15) rgba(0, 0, 0, 0.25);}
-.btn-primary{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(to bottom, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#0044cc;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{color:#ffffff;background-color:#0044cc;*background-color:#003bb3;}
-.btn-primary:active,.btn-primary.active{background-color:#003399 \9;}
-.btn-warning{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#f89406;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{color:#ffffff;background-color:#f89406;*background-color:#df8505;}
-.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
-.btn-danger{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(to bottom, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffbd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#bd362f;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{color:#ffffff;background-color:#bd362f;*background-color:#a9302a;}
-.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
-.btn-success{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(to bottom, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#51a351;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{color:#ffffff;background-color:#51a351;*background-color:#499249;}
-.btn-success:active,.btn-success.active{background-color:#408140 \9;}
-.btn-info{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(to bottom, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#2f96b4;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{color:#ffffff;background-color:#2f96b4;*background-color:#2a85a0;}
-.btn-info:active,.btn-info.active{background-color:#24748c \9;}
-.btn-inverse{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#363636;background-image:-moz-linear-gradient(top, #444444, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#444444), to(#222222));background-image:-webkit-linear-gradient(top, #444444, #222222);background-image:-o-linear-gradient(top, #444444, #222222);background-image:linear-gradient(to bottom, #444444, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff444444', endColorstr='#ff222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#222222;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-inverse:hover,.btn-inverse:active,.btn-inverse.active,.btn-inverse.disabled,.btn-inverse[disabled]{color:#ffffff;background-color:#222222;*background-color:#151515;}
-.btn-inverse:active,.btn-inverse.active{background-color:#080808 \9;}
-button.btn,input[type="submit"].btn{*padding-top:3px;*padding-bottom:3px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
-button.btn.btn-large,input[type="submit"].btn.btn-large{*padding-top:7px;*padding-bottom:7px;}
-button.btn.btn-small,input[type="submit"].btn.btn-small{*padding-top:3px;*padding-bottom:3px;}
-button.btn.btn-mini,input[type="submit"].btn.btn-mini{*padding-top:1px;*padding-bottom:1px;}
-.btn-link,.btn-link:active,.btn-link[disabled]{background-color:transparent;background-image:none;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
-.btn-link{border-color:transparent;cursor:pointer;color:#0088cc;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-link:hover{color:#005580;text-decoration:underline;background-color:transparent;}
-.btn-link[disabled]:hover{color:#333333;text-decoration:none;}
-[class^="icon-"],[class*=" icon-"]{display:inline-block;width:14px;height:14px;*margin-right:.3em;line-height:14px;vertical-align:text-top;background-image:url("../img/glyphicons-halflings.png");background-position:14px 14px;background-repeat:no-repeat;margin-top:1px;}
-.icon-white,.nav-tabs>.active>a>[class^="icon-"],.nav-tabs>.active>a>[class*=" icon-"],.nav-pills>.active>a>[class^="icon-"],.nav-pills>.active>a>[class*=" icon-"],.nav-list>.active>a>[class^="icon-"],.nav-list>.active>a>[class*=" icon-"],.navbar-inverse .nav>.active>a>[class^="icon-"],.navbar-inverse .nav>.active>a>[class*=" icon-"],.dropdown-menu>li>a:hover>[class^="icon-"],.dropdown-menu>li>a:hover>[class*=" icon-"],.dropdown-menu>.active>a>[class^="icon-"],.dropdown-menu>.active>a>[class*=" icon-"]{background-image:url("../img/glyphicons-halflings-white.png");}
-.icon-glass{background-position:0 0;}
-.icon-music{background-position:-24px 0;}
-.icon-search{background-position:-48px 0;}
-.icon-envelope{background-position:-72px 0;}
-.icon-heart{background-position:-96px 0;}
-.icon-star{background-position:-120px 0;}
-.icon-star-empty{background-position:-144px 0;}
-.icon-user{background-position:-168px 0;}
-.icon-film{background-position:-192px 0;}
-.icon-th-large{background-position:-216px 0;}
-.icon-th{background-position:-240px 0;}
-.icon-th-list{background-position:-264px 0;}
-.icon-ok{background-position:-288px 0;}
-.icon-remove{background-position:-312px 0;}
-.icon-zoom-in{background-position:-336px 0;}
-.icon-zoom-out{background-position:-360px 0;}
-.icon-off{background-position:-384px 0;}
-.icon-signal{background-position:-408px 0;}
-.icon-cog{background-position:-432px 0;}
-.icon-trash{background-position:-456px 0;}
-.icon-home{background-position:0 -24px;}
-.icon-file{background-position:-24px -24px;}
-.icon-time{background-position:-48px -24px;}
-.icon-road{background-position:-72px -24px;}
-.icon-download-alt{background-position:-96px -24px;}
-.icon-download{background-position:-120px -24px;}
-.icon-upload{background-position:-144px -24px;}
-.icon-inbox{background-position:-168px -24px;}
-.icon-play-circle{background-position:-192px -24px;}
-.icon-repeat{background-position:-216px -24px;}
-.icon-refresh{background-position:-240px -24px;}
-.icon-list-alt{background-position:-264px -24px;}
-.icon-lock{background-position:-287px -24px;}
-.icon-flag{background-position:-312px -24px;}
-.icon-headphones{background-position:-336px -24px;}
-.icon-volume-off{background-position:-360px -24px;}
-.icon-volume-down{background-position:-384px -24px;}
-.icon-volume-up{background-position:-408px -24px;}
-.icon-qrcode{background-position:-432px -24px;}
-.icon-barcode{background-position:-456px -24px;}
-.icon-tag{background-position:0 -48px;}
-.icon-tags{background-position:-25px -48px;}
-.icon-book{background-position:-48px -48px;}
-.icon-bookmark{background-position:-72px -48px;}
-.icon-print{background-position:-96px -48px;}
-.icon-camera{background-position:-120px -48px;}
-.icon-font{background-position:-144px -48px;}
-.icon-bold{background-position:-167px -48px;}
-.icon-italic{background-position:-192px -48px;}
-.icon-text-height{background-position:-216px -48px;}
-.icon-text-width{background-position:-240px -48px;}
-.icon-align-left{background-position:-264px -48px;}
-.icon-align-center{background-position:-288px -48px;}
-.icon-align-right{background-position:-312px -48px;}
-.icon-align-justify{background-position:-336px -48px;}
-.icon-list{background-position:-360px -48px;}
-.icon-indent-left{background-position:-384px -48px;}
-.icon-indent-right{background-position:-408px -48px;}
-.icon-facetime-video{background-position:-432px -48px;}
-.icon-picture{background-position:-456px -48px;}
-.icon-pencil{background-position:0 -72px;}
-.icon-map-marker{background-position:-24px -72px;}
-.icon-adjust{background-position:-48px -72px;}
-.icon-tint{background-position:-72px -72px;}
-.icon-edit{background-position:-96px -72px;}
-.icon-share{background-position:-120px -72px;}
-.icon-check{background-position:-144px -72px;}
-.icon-move{background-position:-168px -72px;}
-.icon-step-backward{background-position:-192px -72px;}
-.icon-fast-backward{background-position:-216px -72px;}
-.icon-backward{background-position:-240px -72px;}
-.icon-play{background-position:-264px -72px;}
-.icon-pause{background-position:-288px -72px;}
-.icon-stop{background-position:-312px -72px;}
-.icon-forward{background-position:-336px -72px;}
-.icon-fast-forward{background-position:-360px -72px;}
-.icon-step-forward{background-position:-384px -72px;}
-.icon-eject{background-position:-408px -72px;}
-.icon-chevron-left{background-position:-432px -72px;}
-.icon-chevron-right{background-position:-456px -72px;}
-.icon-plus-sign{background-position:0 -96px;}
-.icon-minus-sign{background-position:-24px -96px;}
-.icon-remove-sign{background-position:-48px -96px;}
-.icon-ok-sign{background-position:-72px -96px;}
-.icon-question-sign{background-position:-96px -96px;}
-.icon-info-sign{background-position:-120px -96px;}
-.icon-screenshot{background-position:-144px -96px;}
-.icon-remove-circle{background-position:-168px -96px;}
-.icon-ok-circle{background-position:-192px -96px;}
-.icon-ban-circle{background-position:-216px -96px;}
-.icon-arrow-left{background-position:-240px -96px;}
-.icon-arrow-right{background-position:-264px -96px;}
-.icon-arrow-up{background-position:-289px -96px;}
-.icon-arrow-down{background-position:-312px -96px;}
-.icon-share-alt{background-position:-336px -96px;}
-.icon-resize-full{background-position:-360px -96px;}
-.icon-resize-small{background-position:-384px -96px;}
-.icon-plus{background-position:-408px -96px;}
-.icon-minus{background-position:-433px -96px;}
-.icon-asterisk{background-position:-456px -96px;}
-.icon-exclamation-sign{background-position:0 -120px;}
-.icon-gift{background-position:-24px -120px;}
-.icon-leaf{background-position:-48px -120px;}
-.icon-fire{background-position:-72px -120px;}
-.icon-eye-open{background-position:-96px -120px;}
-.icon-eye-close{background-position:-120px -120px;}
-.icon-warning-sign{background-position:-144px -120px;}
-.icon-plane{background-position:-168px -120px;}
-.icon-calendar{background-position:-192px -120px;}
-.icon-random{background-position:-216px -120px;width:16px;}
-.icon-comment{background-position:-240px -120px;}
-.icon-magnet{background-position:-264px -120px;}
-.icon-chevron-up{background-position:-288px -120px;}
-.icon-chevron-down{background-position:-313px -119px;}
-.icon-retweet{background-position:-336px -120px;}
-.icon-shopping-cart{background-position:-360px -120px;}
-.icon-folder-close{background-position:-384px -120px;}
-.icon-folder-open{background-position:-408px -120px;width:16px;}
-.icon-resize-vertical{background-position:-432px -119px;}
-.icon-resize-horizontal{background-position:-456px -118px;}
-.icon-hdd{background-position:0 -144px;}
-.icon-bullhorn{background-position:-24px -144px;}
-.icon-bell{background-position:-48px -144px;}
-.icon-certificate{background-position:-72px -144px;}
-.icon-thumbs-up{background-position:-96px -144px;}
-.icon-thumbs-down{background-position:-120px -144px;}
-.icon-hand-right{background-position:-144px -144px;}
-.icon-hand-left{background-position:-168px -144px;}
-.icon-hand-up{background-position:-192px -144px;}
-.icon-hand-down{background-position:-216px -144px;}
-.icon-circle-arrow-right{background-position:-240px -144px;}
-.icon-circle-arrow-left{background-position:-264px -144px;}
-.icon-circle-arrow-up{background-position:-288px -144px;}
-.icon-circle-arrow-down{background-position:-312px -144px;}
-.icon-globe{background-position:-336px -144px;}
-.icon-wrench{background-position:-360px -144px;}
-.icon-tasks{background-position:-384px -144px;}
-.icon-filter{background-position:-408px -144px;}
-.icon-briefcase{background-position:-432px -144px;}
-.icon-fullscreen{background-position:-456px -144px;}
-.btn-group{position:relative;font-size:0;vertical-align:middle;white-space:nowrap;*margin-left:.3em;}.btn-group:first-child{*margin-left:0;}
-.btn-group+.btn-group{margin-left:5px;}
-.btn-toolbar{font-size:0;margin-top:10px;margin-bottom:10px;}.btn-toolbar .btn-group{display:inline-block;*display:inline;*zoom:1;}
-.btn-toolbar .btn+.btn,.btn-toolbar .btn-group+.btn,.btn-toolbar .btn+.btn-group{margin-left:5px;}
-.btn-group>.btn{position:relative;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-group>.btn+.btn{margin-left:-1px;}
-.btn-group>.btn,.btn-group>.dropdown-menu{font-size:14px;}
-.btn-group>.btn-mini{font-size:11px;}
-.btn-group>.btn-small{font-size:12px;}
-.btn-group>.btn-large{font-size:16px;}
-.btn-group>.btn:first-child{margin-left:0;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
-.btn-group>.btn:last-child,.btn-group>.dropdown-toggle{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;}
-.btn-group>.btn.large:first-child{margin-left:0;-webkit-border-top-left-radius:6px;-moz-border-radius-topleft:6px;border-top-left-radius:6px;-webkit-border-bottom-left-radius:6px;-moz-border-radius-bottomleft:6px;border-bottom-left-radius:6px;}
-.btn-group>.btn.large:last-child,.btn-group>.large.dropdown-toggle{-webkit-border-top-right-radius:6px;-moz-border-radius-topright:6px;border-top-right-radius:6px;-webkit-border-bottom-right-radius:6px;-moz-border-radius-bottomright:6px;border-bottom-right-radius:6px;}
-.btn-group>.btn:hover,.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active{z-index:2;}
-.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0;}
-.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px;-webkit-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 1px 0 0 rgba(255, 255, 255, 0.125), inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);*padding-top:5px;*padding-bottom:5px;}
-.btn-group>.btn-mini+.dropdown-toggle{padding-left:5px;padding-right:5px;*padding-top:2px;*padding-bottom:2px;}
-.btn-group>.btn-small+.dropdown-toggle{*padding-top:5px;*padding-bottom:4px;}
-.btn-group>.btn-large+.dropdown-toggle{padding-left:12px;padding-right:12px;*padding-top:7px;*padding-bottom:7px;}
-.btn-group.open .dropdown-toggle{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);}
-.btn-group.open .btn.dropdown-toggle{background-color:#e6e6e6;}
-.btn-group.open .btn-primary.dropdown-toggle{background-color:#0044cc;}
-.btn-group.open .btn-warning.dropdown-toggle{background-color:#f89406;}
-.btn-group.open .btn-danger.dropdown-toggle{background-color:#bd362f;}
-.btn-group.open .btn-success.dropdown-toggle{background-color:#51a351;}
-.btn-group.open .btn-info.dropdown-toggle{background-color:#2f96b4;}
-.btn-group.open .btn-inverse.dropdown-toggle{background-color:#222222;}
-.btn .caret{margin-top:8px;margin-left:0;}
-.btn-mini .caret,.btn-small .caret,.btn-large .caret{margin-top:6px;}
-.btn-large .caret{border-left-width:5px;border-right-width:5px;border-top-width:5px;}
-.dropup .btn-large .caret{border-bottom:5px solid #000000;border-top:0;}
-.btn-primary .caret,.btn-warning .caret,.btn-danger .caret,.btn-info .caret,.btn-success .caret,.btn-inverse .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
-.btn-group-vertical{display:inline-block;*display:inline;*zoom:1;}
-.btn-group-vertical .btn{display:block;float:none;width:100%;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.btn-group-vertical .btn+.btn{margin-left:0;margin-top:-1px;}
-.btn-group-vertical .btn:first-child{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
-.btn-group-vertical .btn:last-child{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
-.btn-group-vertical .btn-large:first-child{-webkit-border-radius:6px 6px 0 0;-moz-border-radius:6px 6px 0 0;border-radius:6px 6px 0 0;}
-.btn-group-vertical .btn-large:last-child{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
-.nav{margin-left:0;margin-bottom:20px;list-style:none;}
-.nav>li>a{display:block;}
-.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
-.nav>.pull-right{float:right;}
-.nav-header{display:block;padding:3px 15px;font-size:11px;font-weight:bold;line-height:20px;color:#999999;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);text-transform:uppercase;}
-.nav li+.nav-header{margin-top:9px;}
-.nav-list{padding-left:15px;padding-right:15px;margin-bottom:0;}
-.nav-list>li>a,.nav-list .nav-header{margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
-.nav-list>li>a{padding:3px 15px;}
-.nav-list>.active>a,.nav-list>.active>a:hover{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
-.nav-list [class^="icon-"]{margin-right:2px;}
-.nav-list .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;}
-.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";line-height:0;}
-.nav-tabs:after,.nav-pills:after{clear:both;}
-.nav-tabs>li,.nav-pills>li{float:left;}
-.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
-.nav-tabs{border-bottom:1px solid #ddd;}
-.nav-tabs>li{margin-bottom:-1px;}
-.nav-tabs>li>a{padding-top:8px;padding-bottom:8px;line-height:20px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
-.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
-.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
-.nav-pills>.active>a,.nav-pills>.active>a:hover{color:#ffffff;background-color:#0088cc;}
-.nav-stacked>li{float:none;}
-.nav-stacked>li>a{margin-right:0;}
-.nav-tabs.nav-stacked{border-bottom:0;}
-.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-top-right-radius:4px;-moz-border-radius-topright:4px;border-top-right-radius:4px;-webkit-border-top-left-radius:4px;-moz-border-radius-topleft:4px;border-top-left-radius:4px;}
-.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-bottom-right-radius:4px;-moz-border-radius-bottomright:4px;border-bottom-right-radius:4px;-webkit-border-bottom-left-radius:4px;-moz-border-radius-bottomleft:4px;border-bottom-left-radius:4px;}
-.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
-.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
-.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
-.nav-tabs .dropdown-menu{-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;}
-.nav-pills .dropdown-menu{-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.nav .dropdown-toggle .caret{border-top-color:#0088cc;border-bottom-color:#0088cc;margin-top:6px;}
-.nav .dropdown-toggle:hover .caret{border-top-color:#005580;border-bottom-color:#005580;}
-.nav-tabs .dropdown-toggle .caret{margin-top:8px;}
-.nav .active .dropdown-toggle .caret{border-top-color:#fff;border-bottom-color:#fff;}
-.nav-tabs .active .dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;}
-.nav>.dropdown.active>a:hover{cursor:pointer;}
-.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>li.dropdown.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
-.nav li.dropdown.open .caret,.nav li.dropdown.open.active .caret,.nav li.dropdown.open a:hover .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
-.tabs-stacked .open>a:hover{border-color:#999999;}
-.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";line-height:0;}
-.tabbable:after{clear:both;}
-.tab-content{overflow:auto;}
-.tabs-below>.nav-tabs,.tabs-right>.nav-tabs,.tabs-left>.nav-tabs{border-bottom:0;}
-.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
-.tab-content>.active,.pill-content>.active{display:block;}
-.tabs-below>.nav-tabs{border-top:1px solid #ddd;}
-.tabs-below>.nav-tabs>li{margin-top:-1px;margin-bottom:0;}
-.tabs-below>.nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below>.nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
-.tabs-below>.nav-tabs>.active>a,.tabs-below>.nav-tabs>.active>a:hover{border-color:transparent #ddd #ddd #ddd;}
-.tabs-left>.nav-tabs>li,.tabs-right>.nav-tabs>li{float:none;}
-.tabs-left>.nav-tabs>li>a,.tabs-right>.nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
-.tabs-left>.nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
-.tabs-left>.nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
-.tabs-left>.nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
-.tabs-left>.nav-tabs .active>a,.tabs-left>.nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
-.tabs-right>.nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
-.tabs-right>.nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
-.tabs-right>.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
-.tabs-right>.nav-tabs .active>a,.tabs-right>.nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
-.nav>.disabled>a{color:#999999;}
-.nav>.disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;}
-.navbar{overflow:visible;margin-bottom:20px;color:#777777;*position:relative;*z-index:2;}
-.navbar-inner{min-height:40px;padding-left:20px;padding-right:20px;background-color:#fafafa;background-image:-moz-linear-gradient(top, #ffffff, #f2f2f2);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#f2f2f2));background-image:-webkit-linear-gradient(top, #ffffff, #f2f2f2);background-image:-o-linear-gradient(top, #ffffff, #f2f2f2);background-image:linear-gradient(to bottom, #ffffff, #f2f2f2);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0);border:1px solid #d4d4d4;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);-moz-box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);box-shadow:0 1px 4px rgba(0, 0, 0, 0.065);*zoom:1;}.navbar-inner:before,.navbar-inner:after{display:table;content:"";line-height:0;}
-.navbar-inner:after{clear:both;}
-.navbar .container{width:auto;}
-.nav-collapse.collapse{height:auto;}
-.navbar .brand{float:left;display:block;padding:10px 20px 10px;margin-left:-20px;font-size:20px;font-weight:200;color:#777777;text-shadow:0 1px 0 #ffffff;}.navbar .brand:hover{text-decoration:none;}
-.navbar-text{margin-bottom:0;line-height:40px;}
-.navbar-link{color:#777777;}.navbar-link:hover{color:#333333;}
-.navbar .divider-vertical{height:40px;margin:0 9px;border-left:1px solid #f2f2f2;border-right:1px solid #ffffff;}
-.navbar .btn,.navbar .btn-group{margin-top:5px;}
-.navbar .btn-group .btn,.navbar .input-prepend .btn,.navbar .input-append .btn{margin-top:0;}
-.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";line-height:0;}
-.navbar-form:after{clear:both;}
-.navbar-form input,.navbar-form select,.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
-.navbar-form input,.navbar-form select,.navbar-form .btn{display:inline-block;margin-bottom:0;}
-.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
-.navbar-form .input-append,.navbar-form .input-prepend{margin-top:6px;white-space:nowrap;}.navbar-form .input-append input,.navbar-form .input-prepend input{margin-top:0;}
-.navbar-search{position:relative;float:left;margin-top:5px;margin-bottom:0;}.navbar-search .search-query{margin-bottom:0;padding:4px 14px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.navbar-static-top{position:static;width:100%;margin-bottom:0;}.navbar-static-top .navbar-inner{-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;margin-bottom:0;}
-.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{border-width:0 0 1px;}
-.navbar-fixed-bottom .navbar-inner{border-width:1px 0 0;}
-.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
-.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:940px;}
-.navbar-fixed-top{top:0;}
-.navbar-fixed-top .navbar-inner,.navbar-static-top .navbar-inner{-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.1), 0 1px 10px rgba(0, 0, 0, 0.1);}
-.navbar-fixed-bottom{bottom:0;}.navbar-fixed-bottom .navbar-inner{-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.1), 0 -1px 10px rgba(0, 0, 0, 0.1);}
-.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
-.navbar .nav.pull-right{float:right;margin-right:0;}
-.navbar .nav>li{float:left;}
-.navbar .nav>li>a{float:none;padding:10px 15px 10px;color:#777777;text-decoration:none;text-shadow:0 1px 0 #ffffff;}
-.navbar .nav .dropdown-toggle .caret{margin-top:8px;}
-.navbar .nav>li>a:focus,.navbar .nav>li>a:hover{background-color:transparent;color:#333333;text-decoration:none;}
-.navbar .nav>.active>a,.navbar .nav>.active>a:hover,.navbar .nav>.active>a:focus{color:#555555;text-decoration:none;background-color:#e5e5e5;-webkit-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);-moz-box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);box-shadow:inset 0 3px 8px rgba(0, 0, 0, 0.125);}
-.navbar .btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#ededed;background-image:-moz-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f2f2f2), to(#e5e5e5));background-image:-webkit-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:-o-linear-gradient(top, #f2f2f2, #e5e5e5);background-image:linear-gradient(to bottom, #f2f2f2, #e5e5e5);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2f2f2', endColorstr='#ffe5e5e5', GradientType=0);border-color:#e5e5e5 #e5e5e5 #bfbfbf;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#e5e5e5;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.075);}.navbar .btn-navbar:hover,.navbar .btn-navbar:active,.navbar .btn-navbar.active,.navbar .btn-navbar.disabled,.navbar .btn-navbar[disabled]{color:#ffffff;background-color:#e5e5e5;*background-color:#d9d9d9;}
-.navbar .btn-navbar:active,.navbar .btn-navbar.active{background-color:#cccccc \9;}
-.navbar .btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
-.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
-.navbar .nav>li>.dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
-.navbar .nav>li>.dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
-.navbar-fixed-bottom .nav>li>.dropdown-menu:before{border-top:7px solid #ccc;border-top-color:rgba(0, 0, 0, 0.2);border-bottom:0;bottom:-7px;top:auto;}
-.navbar-fixed-bottom .nav>li>.dropdown-menu:after{border-top:6px solid #ffffff;border-bottom:0;bottom:-6px;top:auto;}
-.navbar .nav li.dropdown.open>.dropdown-toggle,.navbar .nav li.dropdown.active>.dropdown-toggle,.navbar .nav li.dropdown.open.active>.dropdown-toggle{background-color:#e5e5e5;color:#555555;}
-.navbar .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#777777;border-bottom-color:#777777;}
-.navbar .nav li.dropdown.open>.dropdown-toggle .caret,.navbar .nav li.dropdown.active>.dropdown-toggle .caret,.navbar .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#555555;border-bottom-color:#555555;}
-.navbar .pull-right>li>.dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right{left:auto;right:0;}.navbar .pull-right>li>.dropdown-menu:before,.navbar .nav>li>.dropdown-menu.pull-right:before{left:auto;right:12px;}
-.navbar .pull-right>li>.dropdown-menu:after,.navbar .nav>li>.dropdown-menu.pull-right:after{left:auto;right:13px;}
-.navbar .pull-right>li>.dropdown-menu .dropdown-menu,.navbar .nav>li>.dropdown-menu.pull-right .dropdown-menu{left:auto;right:100%;margin-left:0;margin-right:-1px;-webkit-border-radius:6px 0 6px 6px;-moz-border-radius:6px 0 6px 6px;border-radius:6px 0 6px 6px;}
-.navbar-inverse{color:#999999;}.navbar-inverse .navbar-inner{background-color:#1b1b1b;background-image:-moz-linear-gradient(top, #222222, #111111);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111));background-image:-webkit-linear-gradient(top, #222222, #111111);background-image:-o-linear-gradient(top, #222222, #111111);background-image:linear-gradient(to bottom, #222222, #111111);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0);border-color:#252525;}
-.navbar-inverse .brand,.navbar-inverse .nav>li>a{color:#999999;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}.navbar-inverse .brand:hover,.navbar-inverse .nav>li>a:hover{color:#ffffff;}
-.navbar-inverse .nav>li>a:focus,.navbar-inverse .nav>li>a:hover{background-color:transparent;color:#ffffff;}
-.navbar-inverse .nav .active>a,.navbar-inverse .nav .active>a:hover,.navbar-inverse .nav .active>a:focus{color:#ffffff;background-color:#111111;}
-.navbar-inverse .navbar-link{color:#999999;}.navbar-inverse .navbar-link:hover{color:#ffffff;}
-.navbar-inverse .divider-vertical{border-left-color:#111111;border-right-color:#222222;}
-.navbar-inverse .nav li.dropdown.open>.dropdown-toggle,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle{background-color:#111111;color:#ffffff;}
-.navbar-inverse .nav li.dropdown>.dropdown-toggle .caret{border-top-color:#999999;border-bottom-color:#999999;}
-.navbar-inverse .nav li.dropdown.open>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.active>.dropdown-toggle .caret,.navbar-inverse .nav li.dropdown.open.active>.dropdown-toggle .caret{border-top-color:#ffffff;border-bottom-color:#ffffff;}
-.navbar-inverse .navbar-search .search-query{color:#ffffff;background-color:#515151;border-color:#111111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1), 0 1px 0 rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none;}.navbar-inverse .navbar-search .search-query:-moz-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query:-ms-input-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query::-webkit-input-placeholder{color:#cccccc;}
-.navbar-inverse .navbar-search .search-query:focus,.navbar-inverse .navbar-search .search-query.focused{padding:5px 15px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
-.navbar-inverse .btn-navbar{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e0e0e;background-image:-moz-linear-gradient(top, #151515, #040404);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#151515), to(#040404));background-image:-webkit-linear-gradient(top, #151515, #040404);background-image:-o-linear-gradient(top, #151515, #040404);background-image:linear-gradient(to bottom, #151515, #040404);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff151515', endColorstr='#ff040404', GradientType=0);border-color:#040404 #040404 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);*background-color:#040404;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.navbar-inverse .btn-navbar:hover,.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active,.navbar-inverse .btn-navbar.disabled,.navbar-inverse .btn-navbar[disabled]{color:#ffffff;background-color:#040404;*background-color:#000000;}
-.navbar-inverse .btn-navbar:active,.navbar-inverse .btn-navbar.active{background-color:#000000 \9;}
-.breadcrumb{padding:8px 15px;margin:0 0 20px;list-style:none;background-color:#f5f5f5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.breadcrumb li{display:inline-block;*display:inline;*zoom:1;text-shadow:0 1px 0 #ffffff;}
-.breadcrumb .divider{padding:0 5px;color:#ccc;}
-.breadcrumb .active{color:#999999;}
-.pagination{height:40px;margin:20px 0;}
-.pagination ul{display:inline-block;*display:inline;*zoom:1;margin-left:0;margin-bottom:0;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
-.pagination ul>li{display:inline;}
-.pagination ul>li>a,.pagination ul>li>span{float:left;padding:0 14px;line-height:38px;text-decoration:none;background-color:#ffffff;border:1px solid #dddddd;border-left-width:0;}
-.pagination ul>li>a:hover,.pagination ul>.active>a,.pagination ul>.active>span{background-color:#f5f5f5;}
-.pagination ul>.active>a,.pagination ul>.active>span{color:#999999;cursor:default;}
-.pagination ul>.disabled>span,.pagination ul>.disabled>a,.pagination ul>.disabled>a:hover{color:#999999;background-color:transparent;cursor:default;}
-.pagination ul>li:first-child>a,.pagination ul>li:first-child>span{border-left-width:1px;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
-.pagination ul>li:last-child>a,.pagination ul>li:last-child>span{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
-.pagination-centered{text-align:center;}
-.pagination-right{text-align:right;}
-.pager{margin:20px 0;list-style:none;text-align:center;*zoom:1;}.pager:before,.pager:after{display:table;content:"";line-height:0;}
-.pager:after{clear:both;}
-.pager li{display:inline;}
-.pager a,.pager span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}
-.pager a:hover{text-decoration:none;background-color:#f5f5f5;}
-.pager .next a,.pager .next span{float:right;}
-.pager .previous a{float:left;}
-.pager .disabled a,.pager .disabled a:hover,.pager .disabled span{color:#999999;background-color:#fff;cursor:default;}
-.thumbnails{margin-left:-20px;list-style:none;*zoom:1;}.thumbnails:before,.thumbnails:after{display:table;content:"";line-height:0;}
-.thumbnails:after{clear:both;}
-.row-fluid .thumbnails{margin-left:0;}
-.thumbnails>li{float:left;margin-bottom:20px;margin-left:20px;}
-.thumbnail{display:block;padding:4px;line-height:20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);box-shadow:0 1px 3px rgba(0, 0, 0, 0.055);-webkit-transition:all 0.2s ease-in-out;-moz-transition:all 0.2s ease-in-out;-o-transition:all 0.2s ease-in-out;transition:all 0.2s ease-in-out;}
-a.thumbnail:hover{border-color:#0088cc;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}
-.thumbnail>img{display:block;max-width:100%;margin-left:auto;margin-right:auto;}
-.thumbnail .caption{padding:9px;color:#555555;}
-.alert{padding:8px 35px 8px 14px;margin-bottom:20px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;color:#c09853;}
-.alert h4{margin:0;}
-.alert .close{position:relative;top:-2px;right:-21px;line-height:20px;}
-.alert-success{background-color:#dff0d8;border-color:#d6e9c6;color:#468847;}
-.alert-danger,.alert-error{background-color:#f2dede;border-color:#eed3d7;color:#b94a48;}
-.alert-info{background-color:#d9edf7;border-color:#bce8f1;color:#3a87ad;}
-.alert-block{padding-top:14px;padding-bottom:14px;}
-.alert-block>p,.alert-block>ul{margin-bottom:0;}
-.alert-block p+p{margin-top:5px;}
-@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-moz-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-ms-keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}@-o-keyframes progress-bar-stripes{from{background-position:0 0;} to{background-position:40px 0;}}@keyframes progress-bar-stripes{from{background-position:40px 0;} to{background-position:0 0;}}.progress{overflow:hidden;height:20px;margin-bottom:20px;background-color:#f7f7f7;background-image:-moz-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#f5f5f5), to(#f9f9f9));background-image:-webkit-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:-o-linear-gradient(top, #f5f5f5, #f9f9f9);background-image:linear-gradient(to bottom, #f5f5f5, #f9f9f9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#fff9f9f9', GradientType=0);-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.progress .bar{width:0%;height:100%;color:#ffffff;float:left;font-size:12px;text-align:center;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);background-color:#0e90d2;background-image:-moz-linear-gradient(top, #149bdf, #0480be);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#149bdf), to(#0480be));background-image:-webkit-linear-gradient(top, #149bdf, #0480be);background-image:-o-linear-gradient(top, #149bdf, #0480be);background-image:linear-gradient(to bottom, #149bdf, #0480be);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff149bdf', endColorstr='#ff0480be', GradientType=0);-webkit-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 0 -1px 0 rgba(0, 0, 0, 0.15);-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;-webkit-transition:width 0.6s ease;-moz-transition:width 0.6s ease;-o-transition:width 0.6s ease;transition:width 0.6s ease;}
-.progress .bar+.bar{-webkit-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);-moz-box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);box-shadow:inset 1px 0 0 rgba(0, 0, 0, 0.15), inset 0 -1px 0 rgba(0, 0, 0, 0.15);}
-.progress-striped .bar{background-color:#149bdf;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);-webkit-background-size:40px 40px;-moz-background-size:40px 40px;-o-background-size:40px 40px;background-size:40px 40px;}
-.progress.active .bar{-webkit-animation:progress-bar-stripes 2s linear infinite;-moz-animation:progress-bar-stripes 2s linear infinite;-ms-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite;}
-.progress-danger .bar,.progress .bar-danger{background-color:#dd514c;background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(to bottom, #ee5f5b, #c43c35);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffee5f5b', endColorstr='#ffc43c35', GradientType=0);}
-.progress-danger.progress-striped .bar,.progress-striped .bar-danger{background-color:#ee5f5b;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-success .bar,.progress .bar-success{background-color:#5eb95e;background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(to bottom, #62c462, #57a957);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff62c462', endColorstr='#ff57a957', GradientType=0);}
-.progress-success.progress-striped .bar,.progress-striped .bar-success{background-color:#62c462;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-info .bar,.progress .bar-info{background-color:#4bb1cf;background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(to bottom, #5bc0de, #339bb9);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff339bb9', GradientType=0);}
-.progress-info.progress-striped .bar,.progress-striped .bar-info{background-color:#5bc0de;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.progress-warning .bar,.progress .bar-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(to bottom, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffbb450', endColorstr='#fff89406', GradientType=0);}
-.progress-warning.progress-striped .bar,.progress-striped .bar-warning{background-color:#fbb450;background-image:-webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent));background-image:-webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);}
-.hero-unit{padding:60px;margin-bottom:30px;background-color:#eeeeee;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;color:inherit;letter-spacing:-1px;}
-.hero-unit p{font-size:18px;font-weight:200;line-height:30px;color:inherit;}
-.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;padding:5px;font-size:11px;opacity:0;filter:alpha(opacity=0);}.tooltip.in{opacity:0.8;filter:alpha(opacity=80);}
-.tooltip.top{margin-top:-3px;}
-.tooltip.right{margin-left:3px;}
-.tooltip.bottom{margin-top:3px;}
-.tooltip.left{margin-left:-3px;}
-.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid;}
-.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000;}
-.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000;}
-.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000;}
-.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000;}
-.popover{position:absolute;top:0;left:0;z-index:1010;display:none;width:236px;padding:1px;background-color:#ffffff;-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);}.popover.top{margin-bottom:10px;}
-.popover.right{margin-left:10px;}
-.popover.bottom{margin-top:10px;}
-.popover.left{margin-right:10px;}
-.popover-title{margin:0;padding:8px 14px;font-size:14px;font-weight:normal;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;-webkit-border-radius:5px 5px 0 0;-moz-border-radius:5px 5px 0 0;border-radius:5px 5px 0 0;}
-.popover-content{padding:9px 14px;}.popover-content p,.popover-content ul,.popover-content ol{margin-bottom:0;}
-.popover .arrow,.popover .arrow:after{position:absolute;display:inline-block;width:0;height:0;border-color:transparent;border-style:solid;}
-.popover .arrow:after{content:"";z-index:-1;}
-.popover.top .arrow{bottom:-10px;left:50%;margin-left:-10px;border-width:10px 10px 0;border-top-color:#ffffff;}.popover.top .arrow:after{border-width:11px 11px 0;border-top-color:rgba(0, 0, 0, 0.25);bottom:-1px;left:-11px;}
-.popover.right .arrow{top:50%;left:-10px;margin-top:-10px;border-width:10px 10px 10px 0;border-right-color:#ffffff;}.popover.right .arrow:after{border-width:11px 11px 11px 0;border-right-color:rgba(0, 0, 0, 0.25);bottom:-11px;left:-1px;}
-.popover.bottom .arrow{top:-10px;left:50%;margin-left:-10px;border-width:0 10px 10px;border-bottom-color:#ffffff;}.popover.bottom .arrow:after{border-width:0 11px 11px;border-bottom-color:rgba(0, 0, 0, 0.25);top:-1px;left:-11px;}
-.popover.left .arrow{top:50%;right:-10px;margin-top:-10px;border-width:10px 0 10px 10px;border-left-color:#ffffff;}.popover.left .arrow:after{border-width:11px 0 11px 11px;border-left-color:rgba(0, 0, 0, 0.25);bottom:-11px;right:-1px;}
-.modal-open .modal .dropdown-menu{z-index:2050;}
-.modal-open .modal .dropdown.open{*z-index:2050;}
-.modal-open .modal .popover{z-index:2060;}
-.modal-open .modal .tooltip{z-index:2080;}
-.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000;}.modal-backdrop.fade{opacity:0;}
-.modal-backdrop,.modal-backdrop.fade.in{opacity:0.8;filter:alpha(opacity=80);}
-.modal{position:fixed;top:50%;left:50%;z-index:1050;overflow:auto;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
-.modal.fade.in{top:50%;}
-.modal-header{padding:9px 15px;border-bottom:1px solid #eee;}.modal-header .close{margin-top:2px;}
-.modal-header h3{margin:0;line-height:30px;}
-.modal-body{overflow-y:auto;max-height:400px;padding:15px;}
-.modal-form{margin-bottom:0;}
-.modal-footer{padding:14px 15px 15px;margin-bottom:0;text-align:right;background-color:#f5f5f5;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;*zoom:1;}.modal-footer:before,.modal-footer:after{display:table;content:"";line-height:0;}
-.modal-footer:after{clear:both;}
-.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0;}
-.modal-footer .btn-group .btn+.btn{margin-left:-1px;}
-.dropup,.dropdown{position:relative;}
-.dropdown-toggle{*margin-bottom:-3px;}
-.dropdown-toggle:active,.open .dropdown-toggle{outline:0;}
-.caret{display:inline-block;width:0;height:0;vertical-align:top;border-top:4px solid #000000;border-right:4px solid transparent;border-left:4px solid transparent;content:"";}
-.dropdown .caret{margin-top:8px;margin-left:2px;}
-.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;background-color:#ffffff;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.2);*border-right-width:2px;*border-bottom-width:2px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);box-shadow:0 5px 10px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding;background-clip:padding-box;}.dropdown-menu.pull-right{right:0;left:auto;}
-.dropdown-menu .divider{*width:100%;height:1px;margin:9px 1px;*margin:-5px 0 5px;overflow:hidden;background-color:#e5e5e5;border-bottom:1px solid #ffffff;}
-.dropdown-menu a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:20px;color:#333333;white-space:nowrap;}
-.dropdown-menu li>a:hover,.dropdown-menu li>a:focus,.dropdown-submenu:hover>a{text-decoration:none;color:#ffffff;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);}
-.dropdown-menu .active>a,.dropdown-menu .active>a:hover{color:#ffffff;text-decoration:none;outline:0;background-color:#0088cc;background-color:#0081c2;background-image:-moz-linear-gradient(top, #0088cc, #0077b3);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0077b3));background-image:-webkit-linear-gradient(top, #0088cc, #0077b3);background-image:-o-linear-gradient(top, #0088cc, #0077b3);background-image:linear-gradient(to bottom, #0088cc, #0077b3);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0077b3', GradientType=0);}
-.dropdown-menu .disabled>a,.dropdown-menu .disabled>a:hover{color:#999999;}
-.dropdown-menu .disabled>a:hover{text-decoration:none;background-color:transparent;cursor:default;}
-.open{*z-index:1000;}.open >.dropdown-menu{display:block;}
-.pull-right>.dropdown-menu{right:0;left:auto;}
-.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid #000000;content:"";}
-.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px;}
-.dropdown-submenu{position:relative;}
-.dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;}
-.dropdown-submenu:hover>.dropdown-menu{display:block;}
-.dropdown-submenu>a:after{display:block;content:" ";float:right;width:0;height:0;border-color:transparent;border-style:solid;border-width:5px 0 5px 5px;border-left-color:#cccccc;margin-top:5px;margin-right:-10px;}
-.dropdown-submenu:hover>a:after{border-left-color:#ffffff;}
-.dropdown .dropdown-menu .nav-header{padding-left:20px;padding-right:20px;}
-.typeahead{margin-top:2px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.accordion{margin-bottom:20px;}
-.accordion-group{margin-bottom:2px;border:1px solid #e5e5e5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
-.accordion-heading{border-bottom:0;}
-.accordion-heading .accordion-toggle{display:block;padding:8px 15px;}
-.accordion-toggle{cursor:pointer;}
-.accordion-inner{padding:9px 15px;border-top:1px solid #e5e5e5;}
-.carousel{position:relative;margin-bottom:20px;line-height:1;}
-.carousel-inner{overflow:hidden;width:100%;position:relative;}
-.carousel .item{display:none;position:relative;-webkit-transition:0.6s ease-in-out left;-moz-transition:0.6s ease-in-out left;-o-transition:0.6s ease-in-out left;transition:0.6s ease-in-out left;}
-.carousel .item>img{display:block;line-height:1;}
-.carousel .active,.carousel .next,.carousel .prev{display:block;}
-.carousel .active{left:0;}
-.carousel .next,.carousel .prev{position:absolute;top:0;width:100%;}
-.carousel .next{left:100%;}
-.carousel .prev{left:-100%;}
-.carousel .next.left,.carousel .prev.right{left:0;}
-.carousel .active.left{left:-100%;}
-.carousel .active.right{left:100%;}
-.carousel-control{position:absolute;top:40%;left:15px;width:40px;height:40px;margin-top:-20px;font-size:60px;font-weight:100;line-height:30px;color:#ffffff;text-align:center;background:#222222;border:3px solid #ffffff;-webkit-border-radius:23px;-moz-border-radius:23px;border-radius:23px;opacity:0.5;filter:alpha(opacity=50);}.carousel-control.right{left:auto;right:15px;}
-.carousel-control:hover{color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90);}
-.carousel-caption{position:absolute;left:0;right:0;bottom:0;padding:15px;background:#333333;background:rgba(0, 0, 0, 0.75);}
-.carousel-caption h4,.carousel-caption p{color:#ffffff;line-height:20px;}
-.carousel-caption h4{margin:0 0 5px;}
-.carousel-caption p{margin-bottom:0;}
-.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
-.well-large{padding:24px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
-.well-small{padding:9px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
-.close{float:right;font-size:20px;font-weight:bold;line-height:20px;color:#000000;text-shadow:0 1px 0 #ffffff;opacity:0.2;filter:alpha(opacity=20);}.close:hover{color:#000000;text-decoration:none;cursor:pointer;opacity:0.4;filter:alpha(opacity=40);}
-button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none;}
-.pull-right{float:right;}
-.pull-left{float:left;}
-.hide{display:none;}
-.show{display:block;}
-.invisible{visibility:hidden;}
-.affix{position:fixed;}
-.fade{opacity:0;-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;}.fade.in{opacity:1;}
-.collapse{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;-moz-transition:height 0.35s ease;-o-transition:height 0.35s ease;transition:height 0.35s ease;}.collapse.in{height:auto;}
-.hidden{display:none;visibility:hidden;}
-.visible-phone{display:none !important;}
-.visible-tablet{display:none !important;}
-.hidden-desktop{display:none !important;}
-.visible-desktop{display:inherit !important;}
-@media (min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important ;} .visible-tablet{display:inherit !important;} .hidden-tablet{display:none !important;}}@media (max-width:767px){.hidden-desktop{display:inherit !important;} .visible-desktop{display:none !important;} .visible-phone{display:inherit !important;} .hidden-phone{display:none !important;}}@media (max-width:767px){body{padding-left:20px;padding-right:20px;} .navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-left:-20px;margin-right:-20px;} .container-fluid{padding:0;} .dl-horizontal dt{float:none;clear:none;width:auto;text-align:left;} .dl-horizontal dd{margin-left:0;} .container{width:auto;} .row-fluid{width:100%;} .row,.thumbnails{margin-left:0;} .thumbnails>li{float:none;margin-left:0;} [class*="span"],.row-fluid [class*="span"]{float:none;display:block;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;} .input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto;} .controls-row [class*="span"]+[class*="span"]{margin-left:0;} .modal{position:fixed;top:20px;left:20px;right:20px;width:auto;margin:0;}.modal.fade.in{top:auto;}}@media (max-width:480px){.nav-collapse{-webkit-transform:translate3d(0, 0, 0);} .page-header h1 small{display:block;line-height:20px;} input[type="checkbox"],input[type="radio"]{border:1px solid #ccc;} .form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left;} .form-horizontal .controls{margin-left:0;} .form-horizontal .control-list{padding-top:0;} .form-horizontal .form-actions{padding-left:10px;padding-right:10px;} .modal{top:10px;left:10px;right:10px;} .modal-header .close{padding:10px;margin:-10px;} .carousel-caption{position:static;}}@media (min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:20px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px;} .span12{width:724px;} .span11{width:662px;} .span10{width:600px;} .span9{width:538px;} .span8{width:476px;} .span7{width:414px;} .span6{width:352px;} .span5{width:290px;} .span4{width:228px;} .span3{width:166px;} .span2{width:104px;} .span1{width:42px;} .offset12{margin-left:764px;} .offset11{margin-left:702px;} .offset10{margin-left:640px;} .offset9{margin-left:578px;} .offset8{margin-left:516px;} .offset7{margin-left:454px;} .offset6{margin-left:392px;} .offset5{margin-left:330px;} .offset4{margin-left:268px;} .offset3{margin-left:206px;} .offset2{margin-left:144px;} .offset1{margin-left:82px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%;} .row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%;} .row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%;} .row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%;} .row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%;} .row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%;} .row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%;} .row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%;} .row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%;} .row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%;} .row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%;} .row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%;} .row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%;} .row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%;} .row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%;} .row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%;} .row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%;} .row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%;} .row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%;} .row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%;} .row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%;} .row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%;} .row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%;} .row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%;} .row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%;} .row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%;} .row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%;} .row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%;} .row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%;} .row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%;} .row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%;} .row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%;} .row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%;} .row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%;} .row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:20px;} input.span12, textarea.span12, .uneditable-input.span12{width:710px;} input.span11, textarea.span11, .uneditable-input.span11{width:648px;} input.span10, textarea.span10, .uneditable-input.span10{width:586px;} input.span9, textarea.span9, .uneditable-input.span9{width:524px;} input.span8, textarea.span8, .uneditable-input.span8{width:462px;} input.span7, textarea.span7, .uneditable-input.span7{width:400px;} input.span6, textarea.span6, .uneditable-input.span6{width:338px;} input.span5, textarea.span5, .uneditable-input.span5{width:276px;} input.span4, textarea.span4, .uneditable-input.span4{width:214px;} input.span3, textarea.span3, .uneditable-input.span3{width:152px;} input.span2, textarea.span2, .uneditable-input.span2{width:90px;} input.span1, textarea.span1, .uneditable-input.span1{width:28px;}}@media (min-width:1200px){.row{margin-left:-30px;*zoom:1;}.row:before,.row:after{display:table;content:"";line-height:0;} .row:after{clear:both;} [class*="span"]{float:left;min-height:1px;margin-left:30px;} .container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px;} .span12{width:1170px;} .span11{width:1070px;} .span10{width:970px;} .span9{width:870px;} .span8{width:770px;} .span7{width:670px;} .span6{width:570px;} .span5{width:470px;} .span4{width:370px;} .span3{width:270px;} .span2{width:170px;} .span1{width:70px;} .offset12{margin-left:1230px;} .offset11{margin-left:1130px;} .offset10{margin-left:1030px;} .offset9{margin-left:930px;} .offset8{margin-left:830px;} .offset7{margin-left:730px;} .offset6{margin-left:630px;} .offset5{margin-left:530px;} .offset4{margin-left:430px;} .offset3{margin-left:330px;} .offset2{margin-left:230px;} .offset1{margin-left:130px;} .row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";line-height:0;} .row-fluid:after{clear:both;} .row-fluid [class*="span"]{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;float:left;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;} .row-fluid [class*="span"]:first-child{margin-left:0;} .row-fluid .span12{width:100%;*width:99.94680851063829%;} .row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%;} .row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%;} .row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%;} .row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%;} .row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%;} .row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%;} .row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%;} .row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%;} .row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%;} .row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%;} .row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%;} .row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%;} .row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%;} .row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%;} .row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%;} .row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%;} .row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%;} .row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%;} .row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%;} .row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%;} .row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%;} .row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%;} .row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%;} .row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%;} .row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%;} .row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%;} .row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%;} .row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%;} .row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%;} .row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%;} .row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%;} .row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%;} .row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%;} .row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%;} .row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%;} input,textarea,.uneditable-input{margin-left:0;} .controls-row [class*="span"]+[class*="span"]{margin-left:30px;} input.span12, textarea.span12, .uneditable-input.span12{width:1156px;} input.span11, textarea.span11, .uneditable-input.span11{width:1056px;} input.span10, textarea.span10, .uneditable-input.span10{width:956px;} input.span9, textarea.span9, .uneditable-input.span9{width:856px;} input.span8, textarea.span8, .uneditable-input.span8{width:756px;} input.span7, textarea.span7, .uneditable-input.span7{width:656px;} input.span6, textarea.span6, .uneditable-input.span6{width:556px;} input.span5, textarea.span5, .uneditable-input.span5{width:456px;} input.span4, textarea.span4, .uneditable-input.span4{width:356px;} input.span3, textarea.span3, .uneditable-input.span3{width:256px;} input.span2, textarea.span2, .uneditable-input.span2{width:156px;} input.span1, textarea.span1, .uneditable-input.span1{width:56px;} .thumbnails{margin-left:-30px;} .thumbnails>li{margin-left:30px;} .row-fluid .thumbnails{margin-left:0;}}@media (max-width:979px){body{padding-top:0;} .navbar-fixed-top,.navbar-fixed-bottom{position:static;} .navbar-fixed-top{margin-bottom:20px;} .navbar-fixed-bottom{margin-top:20px;} .navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px;} .navbar .container{width:auto;padding:0;} .navbar .brand{padding-left:10px;padding-right:10px;margin:0 0 0 -5px;} .nav-collapse{clear:both;} .nav-collapse .nav{float:none;margin:0 0 10px;} .nav-collapse .nav>li{float:none;} .nav-collapse .nav>li>a{margin-bottom:2px;} .nav-collapse .nav>.divider-vertical{display:none;} .nav-collapse .nav .nav-header{color:#777777;text-shadow:none;} .nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;} .nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;} .nav-collapse .dropdown-menu li+li a{margin-bottom:2px;} .nav-collapse .nav>li>a:hover,.nav-collapse .dropdown-menu a:hover{background-color:#f2f2f2;} .navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:hover{background-color:#111111;} .nav-collapse.in .btn-group{margin-top:5px;padding:0;} .nav-collapse .dropdown-menu{position:static;top:auto;left:auto;float:none;display:block;max-width:none;margin:0 15px;padding:0;background-color:transparent;border:none;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;} .nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none;} .nav-collapse .dropdown-menu .divider{display:none;} .nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none;} .nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);} .navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111111;border-bottom-color:#111111;} .navbar .nav-collapse .nav.pull-right{float:none;margin-left:0;} .nav-collapse,.nav-collapse.collapse{overflow:hidden;height:0;} .navbar .btn-navbar{display:block;} .navbar-static .navbar-inner{padding-left:10px;padding-right:10px;}}@media (min-width:980px){.nav-collapse.collapse{height:auto !important;overflow:visible !important;}}
+ * Bootstrap v3.2.0 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ *//*! normalize.css v3.0.1 | MIT License | git.io/normalize */html{font-family:sans-serif;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:0 0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{margin:.67em 0;font-size:2em}mark{color:#000;background:#ff0}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{height:0;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{margin:0;font:inherit;color:inherit}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0}input{line-height:normal}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;-webkit-appearance:textfield}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{padding:.35em .625em .75em;margin:0 2px;border:1px solid silver}legend{padding:0;border:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-spacing:0;border-collapse:collapse}td,th{padding:0}@media print{*{color:#000!important;text-shadow:none!important;background:transparent!important;-webkit-box-shadow:none!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff!important}.navbar{display:none}.table td,.table th{background-color:#fff!important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000!important}.label{border:1px solid #000}.table{border-collapse:collapse!important}.table-bordered th,.table-bordered td{border:1px solid #ddd!important}}@font-face{font-family:'Glyphicons Halflings';src:url(../fonts/glyphicons-halflings-regular.eot);src:url(../fonts/glyphicons-halflings-regular.eot?#iefix) format('embedded-opentype'),url(../fonts/glyphicons-halflings-regular.woff) format('woff'),url(../fonts/glyphicons-halflings-regular.ttf) format('truetype'),url(../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular) format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}:before,:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:10px;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;line-height:1.42857143;color:#333;background-color:#fff}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#428bca;text-decoration:none}a:hover,a:focus{color:#2a6496;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;width:100% \9;max-width:100%;height:auto}.img-rounded{border-radius:6px}.img-thumbnail{display:inline-block;width:100% \9;max-width:100%;height:auto;padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.img-circle{border-radius:50%}hr{margin-top:20px;margin-bottom:20px;border:0;border-top:1px solid #eee}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:400;line-height:1;color:#777}h1,.h1,h2,.h2,h3,.h3{margin-top:20px;margin-bottom:10px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10px;margin-bottom:10px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:36px}h2,.h2{font-size:30px}h3,.h3{font-size:24px}h4,.h4{font-size:18px}h5,.h5{font-size:14px}h6,.h6{font-size:12px}p{margin:0 0 10px}.lead{margin-bottom:20px;font-size:16px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:21px}}small,.small{font-size:85%}cite{font-style:normal}mark,.mark{padding:.2em;background-color:#fcf8e3}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-nowrap{white-space:nowrap}.text-lowercase{text-transform:lowercase}.text-uppercase{text-transform:uppercase}.text-capitalize{text-transform:capitalize}.text-muted{color:#777}.text-primary{color:#428bca}a.text-primary:hover{color:#3071a9}.text-success{color:#3c763d}a.text-success:hover{color:#2b542c}.text-info{color:#31708f}a.text-info:hover{color:#245269}.text-warning{color:#8a6d3b}a.text-warning:hover{color:#66512c}.text-danger{color:#a94442}a.text-danger:hover{color:#843534}.bg-primary{color:#fff;background-color:#428bca}a.bg-primary:hover{background-color:#3071a9}.bg-success{background-color:#dff0d8}a.bg-success:hover{background-color:#c1e2b3}.bg-info{background-color:#d9edf7}a.bg-info:hover{background-color:#afd9ee}.bg-warning{background-color:#fcf8e3}a.bg-warning:hover{background-color:#f7ecb5}.bg-danger{background-color:#f2dede}a.bg-danger:hover{background-color:#e4b9b9}.page-header{padding-bottom:9px;margin:40px 0 20px;border-bottom:1px solid #eee}ul,ol{margin-top:0;margin-bottom:10px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;margin-left:-5px;list-style:none}.list-inline>li{display:inline-block;padding-right:5px;padding-left:5px}dl{margin-top:0;margin-bottom:20px}dt,dd{line-height:1.42857143}dt{font-weight:700}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;overflow:hidden;clear:left;text-align:right;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10px 20px;margin:0 0 20px;font-size:17.5px;border-left:5px solid #eee}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;text-align:right;border-right:5px solid #eee;border-left:0}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:20px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:4px}kbd{padding:2px 4px;font-size:90%;color:#fff;background-color:#333;border-radius:3px;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.25);box-shadow:inset 0 -1px 0 rgba(0,0,0,.25)}kbd kbd{padding:0;font-size:100%;-webkit-box-shadow:none;box-shadow:none}pre{display:block;padding:9.5px;margin:0 0 10px;font-size:13px;line-height:1.42857143;color:#333;word-break:break-all;word-wrap:break-word;background-color:#f5f5f5;border:1px solid #ccc;border-radius:4px}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}.row{margin-right:-15px;margin-left:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-right:15px;padding-left:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:auto}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:auto}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}table{background-color:transparent}th{text-align:left}.table{width:100%;max-width:100%;margin-bottom:20px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #ddd}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #ddd}.table .table{background-color:#fff}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:5px}.table-bordered{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #ddd}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#f9f9f9}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#f5f5f5}table col[class*=col-]{position:static;display:table-column;float:none}table td[class*=col-],table th[class*=col-]{position:static;display:table-cell;float:none}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#f5f5f5}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr:hover>.active,.table-hover>tbody>tr.active:hover>th{background-color:#e8e8e8}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#dff0d8}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr:hover>.success,.table-hover>tbody>tr.success:hover>th{background-color:#d0e9c6}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#d9edf7}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr:hover>.info,.table-hover>tbody>tr.info:hover>th{background-color:#c4e3f3}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#fcf8e3}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr:hover>.warning,.table-hover>tbody>tr.warning:hover>th{background-color:#faf2cc}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#f2dede}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr:hover>.danger,.table-hover>tbody>tr.danger:hover>th{background-color:#ebcccc}@media screen and (max-width:767px){.table-responsive{width:100%;margin-bottom:15px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #ddd}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;padding:0;margin-bottom:20px;font-size:21px;line-height:inherit;color:#333;border:0;border-bottom:1px solid #e5e5e5}label{display:inline-block;max-width:100%;margin-bottom:5px;font-weight:700}input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=radio],input[type=checkbox]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type=file]{display:block}input[type=range]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type=file]:focus,input[type=radio]:focus,input[type=checkbox]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:7px;font-size:14px;line-height:1.42857143;color:#555}.form-control{display:block;width:100%;height:34px;padding:6px 12px;font-size:14px;line-height:1.42857143;color:#555;background-color:#fff;background-image:none;border:1px solid #ccc;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075);-webkit-transition:border-color ease-in-out .15s,-webkit-box-shadow ease-in-out .15s;-o-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:#66afe9;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6);box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 8px rgba(102,175,233,.6)}.form-control::-moz-placeholder{color:#777;opacity:1}.form-control:-ms-input-placeholder{color:#777}.form-control::-webkit-input-placeholder{color:#777}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#eee;opacity:1}textarea.form-control{height:auto}input[type=search]{-webkit-appearance:none}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{line-height:34px;line-height:1.42857143 \0}input[type=date].input-sm,input[type=time].input-sm,input[type=datetime-local].input-sm,input[type=month].input-sm{line-height:30px}input[type=date].input-lg,input[type=time].input-lg,input[type=datetime-local].input-lg,input[type=month].input-lg{line-height:46px}.form-group{margin-bottom:15px}.radio,.checkbox{position:relative;display:block;min-height:20px;margin-top:10px;margin-bottom:10px}.radio label,.checkbox label{padding-left:20px;margin-bottom:0;font-weight:400;cursor:pointer}.radio input[type=radio],.radio-inline input[type=radio],.checkbox input[type=checkbox],.checkbox-inline input[type=checkbox]{position:absolute;margin-top:4px \9;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;font-weight:400;vertical-align:middle;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type=radio][disabled],input[type=checkbox][disabled],input[type=radio].disabled,input[type=checkbox].disabled,fieldset[disabled] input[type=radio],fieldset[disabled] input[type=checkbox]{cursor:not-allowed}.radio-inline.disabled,.checkbox-inline.disabled,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.radio.disabled label,.checkbox.disabled label,fieldset[disabled] .radio label,fieldset[disabled] .checkbox label{cursor:not-allowed}.form-control-static{padding-top:7px;padding-bottom:7px;margin-bottom:0}.form-control-static.input-lg,.form-control-static.input-sm{padding-right:0;padding-left:0}.input-sm,.form-horizontal .form-group-sm .form-control{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-sm{height:30px;line-height:30px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg,.form-horizontal .form-group-lg .form-control{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-lg{height:46px;line-height:46px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:42.5px}.form-control-feedback{position:absolute;top:25px;right:0;z-index:2;display:block;width:34px;height:34px;line-height:34px;text-align:center}.input-lg+.form-control-feedback{width:46px;height:46px;line-height:46px}.input-sm+.form-control-feedback{width:30px;height:30px;line-height:30px}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#3c763d}.has-success .form-control{border-color:#3c763d;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-success .form-control:focus{border-color:#2b542c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #67b168}.has-success .input-group-addon{color:#3c763d;background-color:#dff0d8;border-color:#3c763d}.has-success .form-control-feedback{color:#3c763d}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#8a6d3b}.has-warning .form-control{border-color:#8a6d3b;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-warning .form-control:focus{border-color:#66512c;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #c0a16b}.has-warning .input-group-addon{color:#8a6d3b;background-color:#fcf8e3;border-color:#8a6d3b}.has-warning .form-control-feedback{color:#8a6d3b}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#a94442}.has-error .form-control{border-color:#a94442;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 1px rgba(0,0,0,.075)}.has-error .form-control:focus{border-color:#843534;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483;box-shadow:inset 0 1px 1px rgba(0,0,0,.075),0 0 6px #ce8483}.has-error .input-group-addon{color:#a94442;background-color:#f2dede;border-color:#a94442}.has-error .form-control-feedback{color:#a94442}.has-feedback label.sr-only~.form-control-feedback{top:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#737373}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group{display:inline-table;vertical-align:middle}.form-inline .input-group .input-group-addon,.form-inline .input-group .input-group-btn,.form-inline .input-group .form-control{width:auto}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.form-inline .radio label,.form-inline .checkbox label{padding-left:0}.form-inline .radio input[type=radio],.form-inline .checkbox input[type=checkbox]{position:relative;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{padding-top:7px;margin-top:0;margin-bottom:0}.form-horizontal .radio,.form-horizontal .checkbox{min-height:27px}.form-horizontal .form-group{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.form-horizontal .control-label{padding-top:7px;margin-bottom:0;text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}@media (min-width:768px){.form-horizontal .form-group-lg .control-label{padding-top:14.3px}}@media (min-width:768px){.form-horizontal .form-group-sm .control-label{padding-top:6px}}.btn{display:inline-block;padding:6px 12px;margin-bottom:0;font-size:14px;font-weight:400;line-height:1.42857143;text-align:center;white-space:nowrap;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-image:none;border:1px solid transparent;border-radius:4px}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#333;text-decoration:none}.btn:active,.btn.active{background-image:none;outline:0;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{pointer-events:none;cursor:not-allowed;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none;opacity:.65}.btn-default{color:#333;background-color:#fff;border-color:#ccc}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{color:#333;background-color:#e6e6e6;border-color:#adadad}.btn-default:active,.btn-default.active,.open>.dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#fff;border-color:#ccc}.btn-default .badge{color:#fff;background-color:#333}.btn-primary{color:#fff;background-color:#428bca;border-color:#357ebd}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{color:#fff;background-color:#3071a9;border-color:#285e8e}.btn-primary:active,.btn-primary.active,.open>.dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#428bca;border-color:#357ebd}.btn-primary .badge{color:#428bca;background-color:#fff}.btn-success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{color:#fff;background-color:#449d44;border-color:#398439}.btn-success:active,.btn-success.active,.open>.dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:#4cae4c}.btn-success .badge{color:#5cb85c;background-color:#fff}.btn-info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{color:#fff;background-color:#31b0d5;border-color:#269abc}.btn-info:active,.btn-info.active,.open>.dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:#46b8da}.btn-info .badge{color:#5bc0de;background-color:#fff}.btn-warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{color:#fff;background-color:#ec971f;border-color:#d58512}.btn-warning:active,.btn-warning.active,.open>.dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:#eea236}.btn-warning .badge{color:#f0ad4e;background-color:#fff}.btn-danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.btn-danger:active,.btn-danger.active,.open>.dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:#d43f3a}.btn-danger .badge{color:#d9534f;background-color:#fff}.btn-link{font-weight:400;color:#428bca;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#2a6496;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#777;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:12px;line-height:1.5;border-radius:3px}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:5px}input[type=submit].btn-block,input[type=reset].btn-block,input[type=button].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity .15s linear;-o-transition:opacity .15s linear;transition:opacity .15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}tr.collapse.in{display:table-row}tbody.collapse.in{display:table-row-group}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height .35s ease;-o-transition:height .35s ease;transition:height .35s ease}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;font-size:14px;text-align:left;list-style:none;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.15);border-radius:4px;-webkit-box-shadow:0 6px 12px rgba(0,0,0,.175);box-shadow:0 6px 12px rgba(0,0,0,.175)}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:400;line-height:1.42857143;color:#333;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{color:#262626;text-decoration:none;background-color:#f5f5f5}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#fff;text-decoration:none;background-color:#428bca;outline:0}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#777}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;cursor:not-allowed;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false)}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{right:0;left:auto}.dropdown-menu-left{right:auto;left:0}.dropdown-header{display:block;padding:3px 20px;font-size:12px;line-height:1.42857143;color:#777;white-space:nowrap}.dropdown-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{content:"";border-top:0;border-bottom:4px solid}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{right:0;left:auto}.navbar-right .dropdown-menu-left{right:auto;left:0}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:0}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-top-left-radius:0;border-bottom-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-right:8px;padding-left:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-right:12px;padding-left:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-top-left-radius:0;border-top-right-radius:0;border-bottom-left-radius:4px}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-left-radius:0;border-top-right-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{display:table-cell;float:none;width:1%}.btn-group-justified>.btn-group .btn{width:100%}.btn-group-justified>.btn-group .dropdown-menu{left:auto}[data-toggle=buttons]>.btn>input[type=radio],[data-toggle=buttons]>.btn>input[type=checkbox]{position:absolute;z-index:-1;filter:alpha(opacity=0);opacity:0}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:46px;padding:10px 16px;font-size:18px;line-height:1.33;border-radius:6px}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:46px;line-height:46px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:30px;padding:5px 10px;font-size:12px;line-height:1.5;border-radius:3px}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:30px;line-height:30px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:6px 12px;font-size:14px;font-weight:400;line-height:1;color:#555;text-align:center;background-color:#eee;border:1px solid #ccc;border-radius:4px}.input-group-addon.input-sm{padding:5px 10px;font-size:12px;border-radius:3px}.input-group-addon.input-lg{padding:10px 16px;font-size:18px;border-radius:6px}.input-group-addon input[type=radio],.input-group-addon input[type=checkbox]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{padding-left:0;margin-bottom:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#eee}.nav>li.disabled>a{color:#777}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#777;text-decoration:none;cursor:not-allowed;background-color:transparent}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#eee;border-color:#428bca}.nav .nav-divider{height:1px;margin:9px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid #ddd}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:4px 4px 0 0}.nav-tabs>li>a:hover{border-color:#eee #eee #ddd}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#555;cursor:default;background-color:#fff;border:1px solid #ddd;border-bottom-color:transparent}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#fff}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:4px}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#fff;background-color:#428bca}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{margin-bottom:5px;text-align:center}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:4px}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #ddd}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #ddd;border-radius:4px 4px 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#fff}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.navbar{position:relative;min-height:50px;margin-bottom:20px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:4px}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{padding-right:15px;padding-left:15px;overflow-x:visible;-webkit-overflow-scrolling:touch;border-top:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1)}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;-webkit-box-shadow:none;box-shadow:none}.navbar-collapse.collapse{display:block!important;height:auto!important;padding-bottom:0;overflow:visible!important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-right:0;padding-left:0}}.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:340px}@media (max-width:480px) and (orientation:landscape){.navbar-fixed-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{max-height:200px}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;height:50px;padding:15px 15px;font-size:18px;line-height:20px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;padding:9px 10px;margin-top:8px;margin-right:15px;margin-bottom:8px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:4px}.navbar-toggle:focus{outline:0}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:7.5px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:20px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:20px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:15px;padding-bottom:15px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left!important}.navbar-right{float:right!important}}.navbar-form{padding:10px 15px;margin-top:8px;margin-right:-15px;margin-bottom:8px;margin-left:-15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 0 rgba(255,255,255,.1),0 1px 0 rgba(255,255,255,.1)}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group{display:inline-table;vertical-align:middle}.navbar-form .input-group .input-group-addon,.navbar-form .input-group .input-group-btn,.navbar-form .input-group .form-control{width:auto}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;vertical-align:middle}.navbar-form .radio label,.navbar-form .checkbox label{padding-left:0}.navbar-form .radio input[type=radio],.navbar-form .checkbox input[type=checkbox]{position:relative;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;padding-top:0;padding-bottom:0;margin-right:0;margin-left:0;border:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-left-radius:0;border-top-right-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:8px;margin-bottom:8px}.navbar-btn.btn-sm{margin-top:10px;margin-bottom:10px}.navbar-btn.btn-xs{margin-top:14px;margin-bottom:14px}.navbar-text{margin-top:15px;margin-bottom:15px}@media (min-width:768px){.navbar-text{float:left;margin-right:15px;margin-left:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#f8f8f8;border-color:#e7e7e7}.navbar-default .navbar-brand{color:#777}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#5e5e5e;background-color:transparent}.navbar-default .navbar-text{color:#777}.navbar-default .navbar-nav>li>a{color:#777}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#ccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:#ddd}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#ddd}.navbar-default .navbar-toggle .icon-bar{background-color:#888}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:#e7e7e7}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{color:#555;background-color:#e7e7e7}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#333;background-color:transparent}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#555;background-color:#e7e7e7}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#ccc;background-color:transparent}}.navbar-default .navbar-link{color:#777}.navbar-default .navbar-link:hover{color:#333}.navbar-default .btn-link{color:#777}.navbar-default .btn-link:hover,.navbar-default .btn-link:focus{color:#333}.navbar-default .btn-link[disabled]:hover,fieldset[disabled] .navbar-default .btn-link:hover,.navbar-default .btn-link[disabled]:focus,fieldset[disabled] .navbar-default .btn-link:focus{color:#ccc}.navbar-inverse{background-color:#222;border-color:#080808}.navbar-inverse .navbar-brand{color:#777}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-text{color:#777}.navbar-inverse .navbar-nav>li>a{color:#777}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:#333}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#333}.navbar-inverse .navbar-toggle .icon-bar{background-color:#fff}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#101010}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{color:#fff;background-color:#080808}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#777}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#fff;background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#fff;background-color:#080808}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444;background-color:transparent}}.navbar-inverse .navbar-link{color:#777}.navbar-inverse .navbar-link:hover{color:#fff}.navbar-inverse .btn-link{color:#777}.navbar-inverse .btn-link:hover,.navbar-inverse .btn-link:focus{color:#fff}.navbar-inverse .btn-link[disabled]:hover,fieldset[disabled] .navbar-inverse .btn-link:hover,.navbar-inverse .btn-link[disabled]:focus,fieldset[disabled] .navbar-inverse .btn-link:focus{color:#444}.breadcrumb{padding:8px 15px;margin-bottom:20px;list-style:none;background-color:#f5f5f5;border-radius:4px}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{padding:0 5px;color:#ccc;content:"/\00a0"}.breadcrumb>.active{color:#777}.pagination{display:inline-block;padding-left:0;margin:20px 0;border-radius:4px}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:6px 12px;margin-left:-1px;line-height:1.42857143;color:#428bca;text-decoration:none;background-color:#fff;border:1px solid #ddd}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-top-left-radius:4px;border-bottom-left-radius:4px}.pagination>li:last-child>a,.pagination>li:last-child>span{border-top-right-radius:4px;border-bottom-right-radius:4px}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#2a6496;background-color:#eee;border-color:#ddd}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#fff;cursor:default;background-color:#428bca;border-color:#428bca}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#777;cursor:not-allowed;background-color:#fff;border-color:#ddd}.pagination-lg>li>a,.pagination-lg>li>span{padding:10px 16px;font-size:18px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-top-left-radius:6px;border-bottom-left-radius:6px}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-top-right-radius:6px;border-bottom-right-radius:6px}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:12px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-top-left-radius:3px;border-bottom-left-radius:3px}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-top-right-radius:3px;border-bottom-right-radius:3px}.pager{padding-left:0;margin:20px 0;text-align:center;list-style:none}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#fff;border:1px solid #ddd;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#eee}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#777;cursor:not-allowed;background-color:#fff}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}a.label:hover,a.label:focus{color:#fff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#777}.label-default[href]:hover,.label-default[href]:focus{background-color:#5e5e5e}.label-primary{background-color:#428bca}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#3071a9}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:12px;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;background-color:#777;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#fff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#428bca;background-color:#fff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#eee}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:21px;font-weight:200}.jumbotron>hr{border-top-color:#d5d5d5}.container .jumbotron{border-radius:6px}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-right:60px;padding-left:60px}.jumbotron h1,.jumbotron .h1{font-size:63px}}.thumbnail{display:block;padding:4px;margin-bottom:20px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:4px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-right:auto;margin-left:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#428bca}.thumbnail .caption{padding:9px;color:#333}.alert{padding:15px;margin-bottom:20px;border:1px solid transparent;border-radius:4px}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:700}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable,.alert-dismissible{padding-right:35px}.alert-dismissable .close,.alert-dismissible .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.alert-success hr{border-top-color:#c9e2b3}.alert-success .alert-link{color:#2b542c}.alert-info{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.alert-info hr{border-top-color:#a6e1ec}.alert-info .alert-link{color:#245269}.alert-warning{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.alert-warning hr{border-top-color:#f7e1b5}.alert-warning .alert-link{color:#66512c}.alert-danger{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.alert-danger hr{border-top-color:#e4b9c0}.alert-danger .alert-link{color:#843534}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@-o-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{height:20px;margin-bottom:20px;overflow:hidden;background-color:#f5f5f5;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,.1);box-shadow:inset 0 1px 2px rgba(0,0,0,.1)}.progress-bar{float:left;width:0;height:100%;font-size:12px;line-height:20px;color:#fff;text-align:center;background-color:#428bca;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,.15);-webkit-transition:width .6s ease;-o-transition:width .6s ease;transition:width .6s ease}.progress-striped .progress-bar,.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);-webkit-background-size:40px 40px;background-size:40px 40px}.progress.active .progress-bar,.progress-bar.active{-webkit-animation:progress-bar-stripes 2s linear infinite;-o-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar[aria-valuenow="1"],.progress-bar[aria-valuenow="2"]{min-width:30px}.progress-bar[aria-valuenow="0"]{min-width:30px;color:#777;background-color:transparent;background-image:none;-webkit-box-shadow:none;box-shadow:none}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{padding-left:0;margin-bottom:20px}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#fff;border:1px solid #ddd}.list-group-item:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#555}a.list-group-item .list-group-item-heading{color:#333}a.list-group-item:hover,a.list-group-item:focus{color:#555;text-decoration:none;background-color:#f5f5f5}.list-group-item.disabled,.list-group-item.disabled:hover,.list-group-item.disabled:focus{color:#777;background-color:#eee}.list-group-item.disabled .list-group-item-heading,.list-group-item.disabled:hover .list-group-item-heading,.list-group-item.disabled:focus .list-group-item-heading{color:inherit}.list-group-item.disabled .list-group-item-text,.list-group-item.disabled:hover .list-group-item-text,.list-group-item.disabled:focus .list-group-item-text{color:#777}.list-group-item.active,.list-group-item.active:hover,.list-group-item.active:focus{z-index:2;color:#fff;background-color:#428bca;border-color:#428bca}.list-group-item.active .list-group-item-heading,.list-group-item.active:hover .list-group-item-heading,.list-group-item.active:focus .list-group-item-heading,.list-group-item.active .list-group-item-heading>small,.list-group-item.active:hover .list-group-item-heading>small,.list-group-item.active:focus .list-group-item-heading>small,.list-group-item.active .list-group-item-heading>.small,.list-group-item.active:hover .list-group-item-heading>.small,.list-group-item.active:focus .list-group-item-heading>.small{color:inherit}.list-group-item.active .list-group-item-text,.list-group-item.active:hover .list-group-item-text,.list-group-item.active:focus .list-group-item-text{color:#e1edf7}.list-group-item-success{color:#3c763d;background-color:#dff0d8}a.list-group-item-success{color:#3c763d}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#3c763d;background-color:#d0e9c6}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#3c763d;border-color:#3c763d}.list-group-item-info{color:#31708f;background-color:#d9edf7}a.list-group-item-info{color:#31708f}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#31708f;background-color:#c4e3f3}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#31708f;border-color:#31708f}.list-group-item-warning{color:#8a6d3b;background-color:#fcf8e3}a.list-group-item-warning{color:#8a6d3b}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#8a6d3b;background-color:#faf2cc}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#8a6d3b;border-color:#8a6d3b}.list-group-item-danger{color:#a94442;background-color:#f2dede}a.list-group-item-danger{color:#a94442}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#a94442;background-color:#ebcccc}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#a94442;border-color:#a94442}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:20px;background-color:#fff;border:1px solid transparent;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,.05);box-shadow:0 1px 1px rgba(0,0,0,.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-left-radius:3px;border-top-right-radius:3px}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:16px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#f5f5f5;border-top:1px solid #ddd;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-left-radius:3px;border-top-right-radius:3px}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.list-group+.panel-footer{border-top-width:0}.panel>.table,.panel>.table-responsive>.table,.panel>.panel-collapse>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-left-radius:3px;border-top-right-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:3px}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:3px}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:3px;border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:3px}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:3px}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #ddd}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{margin-bottom:0;border:0}.panel-group{margin-bottom:20px}.panel-group .panel{margin-bottom:0;border-radius:4px}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse>.panel-body{border-top:1px solid #ddd}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid #ddd}.panel-default{border-color:#ddd}.panel-default>.panel-heading{color:#333;background-color:#f5f5f5;border-color:#ddd}.panel-default>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ddd}.panel-default>.panel-heading .badge{color:#f5f5f5;background-color:#333}.panel-default>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ddd}.panel-primary{border-color:#428bca}.panel-primary>.panel-heading{color:#fff;background-color:#428bca;border-color:#428bca}.panel-primary>.panel-heading+.panel-collapse>.panel-body{border-top-color:#428bca}.panel-primary>.panel-heading .badge{color:#428bca;background-color:#fff}.panel-primary>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#428bca}.panel-success{border-color:#d6e9c6}.panel-success>.panel-heading{color:#3c763d;background-color:#dff0d8;border-color:#d6e9c6}.panel-success>.panel-heading+.panel-collapse>.panel-body{border-top-color:#d6e9c6}.panel-success>.panel-heading .badge{color:#dff0d8;background-color:#3c763d}.panel-success>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#d6e9c6}.panel-info{border-color:#bce8f1}.panel-info>.panel-heading{color:#31708f;background-color:#d9edf7;border-color:#bce8f1}.panel-info>.panel-heading+.panel-collapse>.panel-body{border-top-color:#bce8f1}.panel-info>.panel-heading .badge{color:#d9edf7;background-color:#31708f}.panel-info>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#bce8f1}.panel-warning{border-color:#faebcc}.panel-warning>.panel-heading{color:#8a6d3b;background-color:#fcf8e3;border-color:#faebcc}.panel-warning>.panel-heading+.panel-collapse>.panel-body{border-top-color:#faebcc}.panel-warning>.panel-heading .badge{color:#fcf8e3;background-color:#8a6d3b}.panel-warning>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#faebcc}.panel-danger{border-color:#ebccd1}.panel-danger>.panel-heading{color:#a94442;background-color:#f2dede;border-color:#ebccd1}.panel-danger>.panel-heading+.panel-collapse>.panel-body{border-top-color:#ebccd1}.panel-danger>.panel-heading .badge{color:#f2dede;background-color:#a94442}.panel-danger>.panel-footer+.panel-collapse>.panel-body{border-bottom-color:#ebccd1}.embed-responsive{position:relative;display:block;height:0;padding:0;overflow:hidden}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive.embed-responsive-16by9{padding-bottom:56.25%}.embed-responsive.embed-responsive-4by3{padding-bottom:75%}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#f5f5f5;border:1px solid #e3e3e3;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,.15)}.well-lg{padding:24px;border-radius:6px}.well-sm{padding:9px;border-radius:3px}.close{float:right;font-size:21px;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;filter:alpha(opacity=20);opacity:.2}.close:hover,.close:focus{color:#000;text-decoration:none;cursor:pointer;filter:alpha(opacity=50);opacity:.5}button.close{-webkit-appearance:none;padding:0;cursor:pointer;background:0 0;border:0}.modal-open{overflow:hidden}.modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;display:none;overflow:hidden;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out;-webkit-transform:translate3d(0,-25%,0);-o-transform:translate3d(0,-25%,0);transform:translate3d(0,-25%,0)}.modal.in .modal-dialog{-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #999;border:1px solid rgba(0,0,0,.2);border-radius:6px;outline:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,.5);box-shadow:0 3px 9px rgba(0,0,0,.5)}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000}.modal-backdrop.fade{filter:alpha(opacity=0);opacity:0}.modal-backdrop.in{filter:alpha(opacity=50);opacity:.5}.modal-header{min-height:16.43px;padding:15px;border-bottom:1px solid #e5e5e5}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:15px}.modal-footer{padding:15px;text-align:right;border-top:1px solid #e5e5e5}.modal-footer .btn+.btn{margin-bottom:0;margin-left:5px}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,.5);box-shadow:0 5px 15px rgba(0,0,0,.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1070;display:block;font-size:12px;line-height:1.4;visibility:visible;filter:alpha(opacity=0);opacity:0}.tooltip.in{filter:alpha(opacity=90);opacity:.9}.tooltip.top{padding:5px 0;margin-top:-3px}.tooltip.right{padding:0 5px;margin-left:3px}.tooltip.bottom{padding:5px 0;margin-top:3px}.tooltip.left{padding:0 5px;margin-left:-3px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#fff;text-align:center;text-decoration:none;background-color:#000;border-radius:4px}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000}.tooltip.top-right .tooltip-arrow{right:5px;bottom:0;border-width:5px 5px 0;border-top-color:#000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000}.popover{position:absolute;top:0;left:0;z-index:1060;display:none;max-width:276px;padding:1px;text-align:left;white-space:normal;background-color:#fff;-webkit-background-clip:padding-box;background-clip:padding-box;border:1px solid #ccc;border:1px solid rgba(0,0,0,.2);border-radius:6px;-webkit-box-shadow:0 5px 10px rgba(0,0,0,.2);box-shadow:0 5px 10px rgba(0,0,0,.2)}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{padding:8px 14px;margin:0;font-size:14px;font-weight:400;line-height:18px;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{content:"";border-width:10px}.popover.top>.arrow{bottom:-11px;left:50%;margin-left:-11px;border-top-color:#999;border-top-color:rgba(0,0,0,.25);border-bottom-width:0}.popover.top>.arrow:after{bottom:1px;margin-left:-10px;content:" ";border-top-color:#fff;border-bottom-width:0}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-right-color:#999;border-right-color:rgba(0,0,0,.25);border-left-width:0}.popover.right>.arrow:after{bottom:-10px;left:1px;content:" ";border-right-color:#fff;border-left-width:0}.popover.bottom>.arrow{top:-11px;left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:#999;border-bottom-color:rgba(0,0,0,.25)}.popover.bottom>.arrow:after{top:1px;margin-left:-10px;content:" ";border-top-width:0;border-bottom-color:#fff}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:#999;border-left-color:rgba(0,0,0,.25)}.popover.left>.arrow:after{right:1px;bottom:-10px;content:" ";border-right-width:0;border-left-color:#fff}.carousel{position:relative}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner>.item{position:relative;display:none;-webkit-transition:.6s ease-in-out left;-o-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;bottom:0;left:0;width:15%;font-size:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6);filter:alpha(opacity=50);opacity:.5}.carousel-control.left{background-image:-webkit-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.5)),to(rgba(0,0,0,.0001)));background-image:linear-gradient(to right,rgba(0,0,0,.5) 0,rgba(0,0,0,.0001) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);background-repeat:repeat-x}.carousel-control.right{right:0;left:auto;background-image:-webkit-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-o-linear-gradient(left,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.0001)),to(rgba(0,0,0,.5)));background-image:linear-gradient(to right,rgba(0,0,0,.0001) 0,rgba(0,0,0,.5) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);background-repeat:repeat-x}.carousel-control:hover,.carousel-control:focus{color:#fff;text-decoration:none;filter:alpha(opacity=90);outline:0;opacity:.9}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%;margin-left:-10px}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%;margin-right:-10px}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;padding-left:0;margin-left:-30%;text-align:center;list-style:none}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0);border:1px solid #fff;border-radius:10px}.carousel-indicators .active{width:12px;height:12px;margin:0;background-color:#fff}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;font-size:30px}.carousel-control .glyphicon-chevron-left,.carousel-control .icon-prev{margin-left:-15px}.carousel-control .glyphicon-chevron-right,.carousel-control .icon-next{margin-right:-15px}.carousel-caption{right:20%;left:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.dl-horizontal dd:before,.dl-horizontal dd:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{display:table;content:" "}.clearfix:after,.dl-horizontal dd:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-right:auto;margin-left:auto}.pull-right{float:right!important}.pull-left{float:left!important}.hide{display:none!important}.show{display:block!important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none!important;visibility:hidden!important}.affix{position:fixed;-webkit-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none!important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none!important}@media (max-width:767px){.visible-xs{display:block!important}table.visible-xs{display:table}tr.visible-xs{display:table-row!important}th.visible-xs,td.visible-xs{display:table-cell!important}}@media (max-width:767px){.visible-xs-block{display:block!important}}@media (max-width:767px){.visible-xs-inline{display:inline!important}}@media (max-width:767px){.visible-xs-inline-block{display:inline-block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block!important}table.visible-sm{display:table}tr.visible-sm{display:table-row!important}th.visible-sm,td.visible-sm{display:table-cell!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-block{display:block!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline!important}}@media (min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block!important}table.visible-md{display:table}tr.visible-md{display:table-row!important}th.visible-md,td.visible-md{display:table-cell!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-block{display:block!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline!important}}@media (min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block!important}}@media (min-width:1200px){.visible-lg{display:block!important}table.visible-lg{display:table}tr.visible-lg{display:table-row!important}th.visible-lg,td.visible-lg{display:table-cell!important}}@media (min-width:1200px){.visible-lg-block{display:block!important}}@media (min-width:1200px){.visible-lg-inline{display:inline!important}}@media (min-width:1200px){.visible-lg-inline-block{display:inline-block!important}}@media (max-width:767px){.hidden-xs{display:none!important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none!important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none!important}}@media (min-width:1200px){.hidden-lg{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:block!important}table.visible-print{display:table}tr.visible-print{display:table-row!important}th.visible-print,td.visible-print{display:table-cell!important}}.visible-print-block{display:none!important}@media print{.visible-print-block{display:block!important}}.visible-print-inline{display:none!important}@media print{.visible-print-inline{display:inline!important}}.visible-print-inline-block{display:none!important}@media print{.visible-print-inline-block{display:inline-block!important}}@media print{.hidden-print{display:none!important}} \ No newline at end of file
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index 0261a303..4f52cc56 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -3,20 +3,20 @@
content running up underneath it. */
h1 {
- font-weight: 500;
+ font-weight: 500;
}
h2, h3 {
- font-weight: 300;
+ font-weight: 300;
}
.resource-description, .response-info {
- margin-bottom: 2em;
+ margin-bottom: 2em;
}
.version:before {
- content: "v";
- opacity: 0.6;
- padding-right: 0.25em;
+ content: "v";
+ opacity: 0.6;
+ padding-right: 0.25em;
}
.version {
@@ -24,16 +24,20 @@ h2, h3 {
}
.format-option {
- font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
+ font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
}
.button-form {
- float: right;
- margin-right: 1em;
+ float: right;
+ margin-right: 1em;
}
ul.breadcrumb {
- margin: 58px 0 0 0;
+ margin: 70px 0 0 0;
+}
+
+.breadcrumb li.active a {
+ color: #777;
}
form select, form input, form textarea {
@@ -43,17 +47,18 @@ form select, form input, form textarea {
form select[multiple] {
height: 150px;
}
+
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
}
.errorlist {
- margin-top: 0.5em;
+ margin-top: 0.5em;
}
pre {
@@ -64,8 +69,6 @@ pre {
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
}
-
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot
new file mode 100644
index 00000000..4a4ca865
--- /dev/null
+++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.eot
Binary files differ
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg
new file mode 100644
index 00000000..25691af8
--- /dev/null
+++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.svg
@@ -0,0 +1,229 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
+<svg xmlns="http://www.w3.org/2000/svg">
+<metadata></metadata>
+<defs>
+<font id="glyphicons_halflingsregular" horiz-adv-x="1200" >
+<font-face units-per-em="1200" ascent="960" descent="-240" />
+<missing-glyph horiz-adv-x="500" />
+<glyph />
+<glyph />
+<glyph unicode="&#xd;" />
+<glyph unicode=" " />
+<glyph unicode="*" d="M100 500v200h259l-183 183l141 141l183 -183v259h200v-259l183 183l141 -141l-183 -183h259v-200h-259l183 -183l-141 -141l-183 183v-259h-200v259l-183 -183l-141 141l183 183h-259z" />
+<glyph unicode="+" d="M0 400v300h400v400h300v-400h400v-300h-400v-400h-300v400h-400z" />
+<glyph unicode="&#xa0;" />
+<glyph unicode="&#x2000;" horiz-adv-x="652" />
+<glyph unicode="&#x2001;" horiz-adv-x="1304" />
+<glyph unicode="&#x2002;" horiz-adv-x="652" />
+<glyph unicode="&#x2003;" horiz-adv-x="1304" />
+<glyph unicode="&#x2004;" horiz-adv-x="434" />
+<glyph unicode="&#x2005;" horiz-adv-x="326" />
+<glyph unicode="&#x2006;" horiz-adv-x="217" />
+<glyph unicode="&#x2007;" horiz-adv-x="217" />
+<glyph unicode="&#x2008;" horiz-adv-x="163" />
+<glyph unicode="&#x2009;" horiz-adv-x="260" />
+<glyph unicode="&#x200a;" horiz-adv-x="72" />
+<glyph unicode="&#x202f;" horiz-adv-x="260" />
+<glyph unicode="&#x205f;" horiz-adv-x="326" />
+<glyph unicode="&#x20ac;" d="M100 500l100 100h113q0 47 5 100h-218l100 100h135q37 167 112 257q117 141 297 141q242 0 354 -189q60 -103 66 -209h-181q0 55 -25.5 99t-63.5 68t-75 36.5t-67 12.5q-24 0 -52.5 -10t-62.5 -32t-65.5 -67t-50.5 -107h379l-100 -100h-300q-6 -46 -6 -100h406l-100 -100 h-300q9 -74 33 -132t52.5 -91t62 -54.5t59 -29t46.5 -7.5q29 0 66 13t75 37t63.5 67.5t25.5 96.5h174q-31 -172 -128 -278q-107 -117 -274 -117q-205 0 -324 158q-36 46 -69 131.5t-45 205.5h-217z" />
+<glyph unicode="&#x2212;" d="M200 400h900v300h-900v-300z" />
+<glyph unicode="&#x25fc;" horiz-adv-x="500" d="M0 0z" />
+<glyph unicode="&#x2601;" d="M-14 494q0 -80 56.5 -137t135.5 -57h750q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5z" />
+<glyph unicode="&#x2709;" d="M0 100l400 400l200 -200l200 200l400 -400h-1200zM0 300v600l300 -300zM0 1100l600 -603l600 603h-1200zM900 600l300 300v-600z" />
+<glyph unicode="&#x270f;" d="M-13 -13l333 112l-223 223zM187 403l214 -214l614 614l-214 214zM887 1103l214 -214l99 92q13 13 13 32.5t-13 33.5l-153 153q-15 13 -33 13t-33 -13z" />
+<glyph unicode="&#xe001;" d="M0 1200h1200l-500 -550v-550h300v-100h-800v100h300v550z" />
+<glyph unicode="&#xe002;" d="M14 84q18 -55 86 -75.5t147 5.5q65 21 109 69t44 90v606l600 155v-521q-64 16 -138 -7q-79 -26 -122.5 -83t-25.5 -111q18 -55 86 -75.5t147 4.5q70 23 111.5 63.5t41.5 95.5v881q0 10 -7 15.5t-17 2.5l-752 -193q-10 -3 -17 -12.5t-7 -19.5v-689q-64 17 -138 -7 q-79 -25 -122.5 -82t-25.5 -112z" />
+<glyph unicode="&#xe003;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233z" />
+<glyph unicode="&#xe005;" d="M100 784q0 64 28 123t73 100.5t104.5 64t119 20.5t120 -38.5t104.5 -104.5q48 69 109.5 105t121.5 38t118.5 -20.5t102.5 -64t71 -100.5t27 -123q0 -57 -33.5 -117.5t-94 -124.5t-126.5 -127.5t-150 -152.5t-146 -174q-62 85 -145.5 174t-149.5 152.5t-126.5 127.5 t-94 124.5t-33.5 117.5z" />
+<glyph unicode="&#xe006;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1z" />
+<glyph unicode="&#xe007;" d="M-72 800h479l146 400h2l146 -400h472l-382 -278l145 -449l-384 275l-382 -275l146 447zM168 71l2 1zM237 700l196 -142l-73 -226l192 140l195 -141l-74 229l193 140h-235l-77 211l-78 -211h-239z" />
+<glyph unicode="&#xe008;" d="M0 0v143l400 257v100q-37 0 -68.5 74.5t-31.5 125.5v200q0 124 88 212t212 88t212 -88t88 -212v-200q0 -51 -31.5 -125.5t-68.5 -74.5v-100l400 -257v-143h-1200z" />
+<glyph unicode="&#xe009;" d="M0 0v1100h1200v-1100h-1200zM100 100h100v100h-100v-100zM100 300h100v100h-100v-100zM100 500h100v100h-100v-100zM100 700h100v100h-100v-100zM100 900h100v100h-100v-100zM300 100h600v400h-600v-400zM300 600h600v400h-600v-400zM1000 100h100v100h-100v-100z M1000 300h100v100h-100v-100zM1000 500h100v100h-100v-100zM1000 700h100v100h-100v-100zM1000 900h100v100h-100v-100z" />
+<glyph unicode="&#xe010;" d="M0 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM0 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400 q-21 0 -35.5 14.5t-14.5 35.5zM600 50v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5zM600 650v400q0 21 14.5 35.5t35.5 14.5h400q21 0 35.5 -14.5t14.5 -35.5v-400 q0 -21 -14.5 -35.5t-35.5 -14.5h-400q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe011;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200 q-21 0 -35.5 14.5t-14.5 35.5zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 450v200q0 21 14.5 35.5t35.5 14.5h200 q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM800 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe012;" d="M0 50v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM0 450q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v200q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5 t-14.5 -35.5v-200zM0 850v200q0 21 14.5 35.5t35.5 14.5h200q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5zM400 50v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5 t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 450v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5zM400 850v200q0 21 14.5 35.5t35.5 14.5h700q21 0 35.5 -14.5t14.5 -35.5 v-200q0 -21 -14.5 -35.5t-35.5 -14.5h-700q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe013;" d="M29 454l419 -420l818 820l-212 212l-607 -607l-206 207z" />
+<glyph unicode="&#xe014;" d="M106 318l282 282l-282 282l212 212l282 -282l282 282l212 -212l-282 -282l282 -282l-212 -212l-282 282l-282 -282z" />
+<glyph unicode="&#xe015;" d="M23 693q0 200 142 342t342 142t342 -142t142 -342q0 -142 -78 -261l300 -300q7 -8 7 -18t-7 -18l-109 -109q-8 -7 -18 -7t-18 7l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 693q0 -136 97 -233t234 -97t233.5 96.5t96.5 233.5t-96.5 233.5t-233.5 96.5 t-234 -97t-97 -233zM300 600v200h100v100h200v-100h100v-200h-100v-100h-200v100h-100z" />
+<glyph unicode="&#xe016;" d="M23 694q0 200 142 342t342 142t342 -142t142 -342q0 -141 -78 -262l300 -299q7 -7 7 -18t-7 -18l-109 -109q-8 -8 -18 -8t-18 8l-300 300q-119 -78 -261 -78q-200 0 -342 142t-142 342zM176 694q0 -136 97 -233t234 -97t233.5 97t96.5 233t-96.5 233t-233.5 97t-234 -97 t-97 -233zM300 601h400v200h-400v-200z" />
+<glyph unicode="&#xe017;" d="M23 600q0 183 105 331t272 210v-166q-103 -55 -165 -155t-62 -220q0 -177 125 -302t302 -125t302 125t125 302q0 120 -62 220t-165 155v166q167 -62 272 -210t105 -331q0 -118 -45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5 zM500 750q0 -21 14.5 -35.5t35.5 -14.5h100q21 0 35.5 14.5t14.5 35.5v400q0 21 -14.5 35.5t-35.5 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-400z" />
+<glyph unicode="&#xe018;" d="M100 1h200v300h-200v-300zM400 1v500h200v-500h-200zM700 1v800h200v-800h-200zM1000 1v1200h200v-1200h-200z" />
+<glyph unicode="&#xe019;" d="M26 601q0 -33 6 -74l151 -38l2 -6q14 -49 38 -93l3 -5l-80 -134q45 -59 105 -105l133 81l5 -3q45 -26 94 -39l5 -2l38 -151q40 -5 74 -5q27 0 74 5l38 151l6 2q46 13 93 39l5 3l134 -81q56 44 104 105l-80 134l3 5q24 44 39 93l1 6l152 38q5 40 5 74q0 28 -5 73l-152 38 l-1 6q-16 51 -39 93l-3 5l80 134q-44 58 -104 105l-134 -81l-5 3q-45 25 -93 39l-6 1l-38 152q-40 5 -74 5q-27 0 -74 -5l-38 -152l-5 -1q-50 -14 -94 -39l-5 -3l-133 81q-59 -47 -105 -105l80 -134l-3 -5q-25 -47 -38 -93l-2 -6l-151 -38q-6 -48 -6 -73zM385 601 q0 88 63 151t152 63t152 -63t63 -151q0 -89 -63 -152t-152 -63t-152 63t-63 152z" />
+<glyph unicode="&#xe020;" d="M100 1025v50q0 10 7.5 17.5t17.5 7.5h275v100q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5v-100h275q10 0 17.5 -7.5t7.5 -17.5v-50q0 -11 -7 -18t-18 -7h-1050q-11 0 -18 7t-7 18zM200 100v800h900v-800q0 -41 -29.5 -71t-70.5 -30h-700q-41 0 -70.5 30 t-29.5 71zM300 100h100v700h-100v-700zM500 100h100v700h-100v-700zM500 1100h300v100h-300v-100zM700 100h100v700h-100v-700zM900 100h100v700h-100v-700z" />
+<glyph unicode="&#xe021;" d="M1 601l656 644l644 -644h-200v-600h-300v400h-300v-400h-300v600h-200z" />
+<glyph unicode="&#xe022;" d="M100 25v1150q0 11 7 18t18 7h475v-500h400v-675q0 -11 -7 -18t-18 -7h-850q-11 0 -18 7t-7 18zM700 800v300l300 -300h-300z" />
+<glyph unicode="&#xe023;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 500v400h100 v-300h200v-100h-300z" />
+<glyph unicode="&#xe024;" d="M-100 0l431 1200h209l-21 -300h162l-20 300h208l431 -1200h-538l-41 400h-242l-40 -400h-539zM488 500h224l-27 300h-170z" />
+<glyph unicode="&#xe025;" d="M0 0v400h490l-290 300h200v500h300v-500h200l-290 -300h490v-400h-1100zM813 200h175v100h-175v-100z" />
+<glyph unicode="&#xe026;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM188 600q0 -170 121 -291t291 -121t291 121t121 291t-121 291t-291 121 t-291 -121t-121 -291zM350 600h150v300h200v-300h150l-250 -300z" />
+<glyph unicode="&#xe027;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM350 600l250 300 l250 -300h-150v-300h-200v300h-150z" />
+<glyph unicode="&#xe028;" d="M0 25v475l200 700h800l199 -700l1 -475q0 -11 -7 -18t-18 -7h-1150q-11 0 -18 7t-7 18zM200 500h200l50 -200h300l50 200h200l-97 500h-606z" />
+<glyph unicode="&#xe029;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM500 397v401 l297 -200z" />
+<glyph unicode="&#xe030;" d="M23 600q0 -118 45.5 -224.5t123 -184t184 -123t224.5 -45.5t224.5 45.5t184 123t123 184t45.5 224.5h-150q0 -177 -125 -302t-302 -125t-302 125t-125 302t125 302t302 125q136 0 246 -81l-146 -146h400v400l-145 -145q-157 122 -355 122q-118 0 -224.5 -45.5t-184 -123 t-123 -184t-45.5 -224.5z" />
+<glyph unicode="&#xe031;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5q198 0 355 -122l145 145v-400h-400l147 147q-112 80 -247 80q-177 0 -302 -125t-125 -302h-150zM100 0v400h400l-147 -147q112 -80 247 -80q177 0 302 125t125 302h150q0 -118 -45.5 -224.5t-123 -184t-184 -123 t-224.5 -45.5q-198 0 -355 122z" />
+<glyph unicode="&#xe032;" d="M100 0h1100v1200h-1100v-1200zM200 100v900h900v-900h-900zM300 200v100h100v-100h-100zM300 400v100h100v-100h-100zM300 600v100h100v-100h-100zM300 800v100h100v-100h-100zM500 200h500v100h-500v-100zM500 400v100h500v-100h-500zM500 600v100h500v-100h-500z M500 800v100h500v-100h-500z" />
+<glyph unicode="&#xe033;" d="M0 100v600q0 41 29.5 70.5t70.5 29.5h100v200q0 82 59 141t141 59h300q82 0 141 -59t59 -141v-200h100q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-900q-41 0 -70.5 29.5t-29.5 70.5zM400 800h300v150q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-150z" />
+<glyph unicode="&#xe034;" d="M100 0v1100h100v-1100h-100zM300 400q60 60 127.5 84t127.5 17.5t122 -23t119 -30t110 -11t103 42t91 120.5v500q-40 -81 -101.5 -115.5t-127.5 -29.5t-138 25t-139.5 40t-125.5 25t-103 -29.5t-65 -115.5v-500z" />
+<glyph unicode="&#xe035;" d="M0 275q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 127 70.5 231.5t184.5 161.5t245 57t245 -57t184.5 -161.5t70.5 -231.5v-300q0 -11 7 -18t18 -7h50q11 0 18 7t7 18v300q0 116 -49.5 227t-131 192.5t-192.5 131t-227 49.5t-227 -49.5t-192.5 -131t-131 -192.5 t-49.5 -227v-300zM200 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14zM800 20v460q0 8 6 14t14 6h160q8 0 14 -6t6 -14v-460q0 -8 -6 -14t-14 -6h-160q-8 0 -14 6t-6 14z" />
+<glyph unicode="&#xe036;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM688 459l141 141l-141 141l71 71l141 -141l141 141l71 -71l-141 -141l141 -141l-71 -71l-141 141l-141 -141z" />
+<glyph unicode="&#xe037;" d="M0 400h300l300 -200v800l-300 -200h-300v-400zM700 857l69 53q111 -135 111 -310q0 -169 -106 -302l-67 54q86 110 86 248q0 146 -93 257z" />
+<glyph unicode="&#xe038;" d="M0 401v400h300l300 200v-800l-300 200h-300zM702 858l69 53q111 -135 111 -310q0 -170 -106 -303l-67 55q86 110 86 248q0 145 -93 257zM889 951l7 -8q123 -151 123 -344q0 -189 -119 -339l-7 -8l81 -66l6 8q142 178 142 405q0 230 -144 408l-6 8z" />
+<glyph unicode="&#xe039;" d="M0 0h500v500h-200v100h-100v-100h-200v-500zM0 600h100v100h400v100h100v100h-100v300h-500v-600zM100 100v300h300v-300h-300zM100 800v300h300v-300h-300zM200 200v100h100v-100h-100zM200 900h100v100h-100v-100zM500 500v100h300v-300h200v-100h-100v-100h-200v100 h-100v100h100v200h-200zM600 0v100h100v-100h-100zM600 1000h100v-300h200v-300h300v200h-200v100h200v500h-600v-200zM800 800v300h300v-300h-300zM900 0v100h300v-100h-300zM900 900v100h100v-100h-100zM1100 200v100h100v-100h-100z" />
+<glyph unicode="&#xe040;" d="M0 200h100v1000h-100v-1000zM100 0v100h300v-100h-300zM200 200v1000h100v-1000h-100zM500 0v91h100v-91h-100zM500 200v1000h200v-1000h-200zM700 0v91h100v-91h-100zM800 200v1000h100v-1000h-100zM900 0v91h200v-91h-200zM1000 200v1000h200v-1000h-200z" />
+<glyph unicode="&#xe041;" d="M0 700l1 475q0 10 7.5 17.5t17.5 7.5h474l700 -700l-500 -500zM148 953q0 -42 29 -71q30 -30 71.5 -30t71.5 30q29 29 29 71t-29 71q-30 30 -71.5 30t-71.5 -30q-29 -29 -29 -71z" />
+<glyph unicode="&#xe042;" d="M1 700l1 475q0 11 7 18t18 7h474l700 -700l-500 -500zM148 953q0 -42 30 -71q29 -30 71 -30t71 30q30 29 30 71t-30 71q-29 30 -71 30t-71 -30q-30 -29 -30 -71zM701 1200h100l700 -700l-500 -500l-50 50l450 450z" />
+<glyph unicode="&#xe043;" d="M100 0v1025l175 175h925v-1000l-100 -100v1000h-750l-100 -100h750v-1000h-900z" />
+<glyph unicode="&#xe044;" d="M200 0l450 444l450 -443v1150q0 20 -14.5 35t-35.5 15h-800q-21 0 -35.5 -15t-14.5 -35v-1151z" />
+<glyph unicode="&#xe045;" d="M0 100v700h200l100 -200h600l100 200h200v-700h-200v200h-800v-200h-200zM253 829l40 -124h592l62 124l-94 346q-2 11 -10 18t-18 7h-450q-10 0 -18 -7t-10 -18zM281 24l38 152q2 10 11.5 17t19.5 7h500q10 0 19.5 -7t11.5 -17l38 -152q2 -10 -3.5 -17t-15.5 -7h-600 q-10 0 -15.5 7t-3.5 17z" />
+<glyph unicode="&#xe046;" d="M0 200q0 -41 29.5 -70.5t70.5 -29.5h1000q41 0 70.5 29.5t29.5 70.5v600q0 41 -29.5 70.5t-70.5 29.5h-150q-4 8 -11.5 21.5t-33 48t-53 61t-69 48t-83.5 21.5h-200q-41 0 -82 -20.5t-70 -50t-52 -59t-34 -50.5l-12 -20h-150q-41 0 -70.5 -29.5t-29.5 -70.5v-600z M356 500q0 100 72 172t172 72t172 -72t72 -172t-72 -172t-172 -72t-172 72t-72 172zM494 500q0 -44 31 -75t75 -31t75 31t31 75t-31 75t-75 31t-75 -31t-31 -75zM900 700v100h100v-100h-100z" />
+<glyph unicode="&#xe047;" d="M53 0h365v66q-41 0 -72 11t-49 38t1 71l92 234h391l82 -222q16 -45 -5.5 -88.5t-74.5 -43.5v-66h417v66q-34 1 -74 43q-18 19 -33 42t-21 37l-6 13l-385 998h-93l-399 -1006q-24 -48 -52 -75q-12 -12 -33 -25t-36 -20l-15 -7v-66zM416 521l178 457l46 -140l116 -317h-340 z" />
+<glyph unicode="&#xe048;" d="M100 0v89q41 7 70.5 32.5t29.5 65.5v827q0 28 -1 39.5t-5.5 26t-15.5 21t-29 14t-49 14.5v71l471 -1q120 0 213 -88t93 -228q0 -55 -11.5 -101.5t-28 -74t-33.5 -47.5t-28 -28l-12 -7q8 -3 21.5 -9t48 -31.5t60.5 -58t47.5 -91.5t21.5 -129q0 -84 -59 -156.5t-142 -111 t-162 -38.5h-500zM400 200h161q89 0 153 48.5t64 132.5q0 90 -62.5 154.5t-156.5 64.5h-159v-400zM400 700h139q76 0 130 61.5t54 138.5q0 82 -84 130.5t-239 48.5v-379z" />
+<glyph unicode="&#xe049;" d="M200 0v57q77 7 134.5 40.5t65.5 80.5l173 849q10 56 -10 74t-91 37q-6 1 -10.5 2.5t-9.5 2.5v57h425l2 -57q-33 -8 -62 -25.5t-46 -37t-29.5 -38t-17.5 -30.5l-5 -12l-128 -825q-10 -52 14 -82t95 -36v-57h-500z" />
+<glyph unicode="&#xe050;" d="M-75 200h75v800h-75l125 167l125 -167h-75v-800h75l-125 -167zM300 900v300h150h700h150v-300h-50q0 29 -8 48.5t-18.5 30t-33.5 15t-39.5 5.5t-50.5 1h-200v-850l100 -50v-100h-400v100l100 50v850h-200q-34 0 -50.5 -1t-40 -5.5t-33.5 -15t-18.5 -30t-8.5 -48.5h-49z " />
+<glyph unicode="&#xe051;" d="M33 51l167 125v-75h800v75l167 -125l-167 -125v75h-800v-75zM100 901v300h150h700h150v-300h-50q0 29 -8 48.5t-18 30t-33.5 15t-40 5.5t-50.5 1h-200v-650l100 -50v-100h-400v100l100 50v650h-200q-34 0 -50.5 -1t-39.5 -5.5t-33.5 -15t-18.5 -30t-8 -48.5h-50z" />
+<glyph unicode="&#xe052;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 350q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM0 650q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1000q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 950q0 -20 14.5 -35t35.5 -15h600q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-600q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe053;" d="M0 50q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM0 650q0 -20 14.5 -35t35.5 -15h1100q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5 v-100zM200 350q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM200 950q0 -20 14.5 -35t35.5 -15h700q21 0 35.5 15t14.5 35v100q0 21 -14.5 35.5t-35.5 14.5h-700q-21 0 -35.5 -14.5 t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe054;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM100 650v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1000q-21 0 -35.5 15 t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM500 950v100q0 21 14.5 35.5t35.5 14.5h600q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-600 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe055;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h1100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-1100 q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe056;" d="M0 50v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 350v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM0 650v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15t-14.5 35zM0 950v100q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-100q-21 0 -35.5 15 t-14.5 35zM300 50v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 350v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800 q-21 0 -35.5 15t-14.5 35zM300 650v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15h-800q-21 0 -35.5 15t-14.5 35zM300 950v100q0 21 14.5 35.5t35.5 14.5h800q21 0 35.5 -14.5t14.5 -35.5v-100q0 -20 -14.5 -35t-35.5 -15 h-800q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe057;" d="M-101 500v100h201v75l166 -125l-166 -125v75h-201zM300 0h100v1100h-100v-1100zM500 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35 v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 650q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM500 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100 q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100z" />
+<glyph unicode="&#xe058;" d="M1 50q0 -20 14.5 -35t35.5 -15h600q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-600q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 350q0 -20 14.5 -35t35.5 -15h300q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-300q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 650 q0 -20 14.5 -35t35.5 -15h500q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-500q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM1 950q0 -20 14.5 -35t35.5 -15h100q20 0 35 15t15 35v100q0 21 -15 35.5t-35 14.5h-100q-21 0 -35.5 -14.5t-14.5 -35.5v-100zM801 0v1100h100v-1100 h-100zM934 550l167 -125v75h200v100h-200v75z" />
+<glyph unicode="&#xe059;" d="M0 275v650q0 31 22 53t53 22h750q31 0 53 -22t22 -53v-650q0 -31 -22 -53t-53 -22h-750q-31 0 -53 22t-22 53zM900 600l300 300v-600z" />
+<glyph unicode="&#xe060;" d="M0 44v1012q0 18 13 31t31 13h1112q19 0 31.5 -13t12.5 -31v-1012q0 -18 -12.5 -31t-31.5 -13h-1112q-18 0 -31 13t-13 31zM100 263l247 182l298 -131l-74 156l293 318l236 -288v500h-1000v-737zM208 750q0 56 39 95t95 39t95 -39t39 -95t-39 -95t-95 -39t-95 39t-39 95z " />
+<glyph unicode="&#xe062;" d="M148 745q0 124 60.5 231.5t165 172t226.5 64.5q123 0 227 -63t164.5 -169.5t60.5 -229.5t-73 -272q-73 -114 -166.5 -237t-150.5 -189l-57 -66q-10 9 -27 26t-66.5 70.5t-96 109t-104 135.5t-100.5 155q-63 139 -63 262zM342 772q0 -107 75.5 -182.5t181.5 -75.5 q107 0 182.5 75.5t75.5 182.5t-75.5 182t-182.5 75t-182 -75.5t-75 -181.5z" />
+<glyph unicode="&#xe063;" d="M1 600q0 122 47.5 233t127.5 191t191 127.5t233 47.5t233 -47.5t191 -127.5t127.5 -191t47.5 -233t-47.5 -233t-127.5 -191t-191 -127.5t-233 -47.5t-233 47.5t-191 127.5t-127.5 191t-47.5 233zM173 600q0 -177 125.5 -302t301.5 -125v854q-176 0 -301.5 -125 t-125.5 -302z" />
+<glyph unicode="&#xe064;" d="M117 406q0 94 34 186t88.5 172.5t112 159t115 177t87.5 194.5q21 -71 57.5 -142.5t76 -130.5t83 -118.5t82 -117t70 -116t50 -125.5t18.5 -136q0 -89 -39 -165.5t-102 -126.5t-140 -79.5t-156 -33.5q-114 6 -211.5 53t-161.5 139t-64 210zM243 414q14 -82 59.5 -136 t136.5 -80l16 98q-7 6 -18 17t-34 48t-33 77q-15 73 -14 143.5t10 122.5l9 51q-92 -110 -119.5 -185t-12.5 -156z" />
+<glyph unicode="&#xe065;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5q366 -6 397 -14l-186 -186h-311q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v125l200 200v-225q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM436 341l161 50l412 412l-114 113l-405 -405zM995 1015l113 -113l113 113l-21 85l-92 28z" />
+<glyph unicode="&#xe066;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h261l2 -80q-133 -32 -218 -120h-145q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5l200 153v-53q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5 zM423 524q30 38 81.5 64t103 35.5t99 14t77.5 3.5l29 -1v-209l360 324l-359 318v-216q-7 0 -19 -1t-48 -8t-69.5 -18.5t-76.5 -37t-76.5 -59t-62 -88t-39.5 -121.5z" />
+<glyph unicode="&#xe067;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q61 0 127 -23l-178 -177h-349q-41 0 -70.5 -29.5t-29.5 -70.5v-500q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v69l200 200v-169q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5 t-117.5 282.5zM342 632l283 -284l567 567l-137 137l-430 -431l-146 147z" />
+<glyph unicode="&#xe068;" d="M0 603l300 296v-198h200v200h-200l300 300l295 -300h-195v-200h200v198l300 -296l-300 -300v198h-200v-200h195l-295 -300l-300 300h200v200h-200v-198z" />
+<glyph unicode="&#xe069;" d="M200 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-1100l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe070;" d="M0 50v1000q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-437l500 487v-487l500 487v-1100l-500 488v-488l-500 488v-438q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5z" />
+<glyph unicode="&#xe071;" d="M136 550l564 550v-487l500 487v-1100l-500 488v-488z" />
+<glyph unicode="&#xe072;" d="M200 0l900 550l-900 550v-1100z" />
+<glyph unicode="&#xe073;" d="M200 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200q-21 0 -35.5 -14.5t-14.5 -35.5v-800zM600 150q0 -21 14.5 -35.5t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v800q0 21 -14.5 35.5t-35.5 14.5h-200 q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe074;" d="M200 150q0 -20 14.5 -35t35.5 -15h800q21 0 35.5 15t14.5 35v800q0 21 -14.5 35.5t-35.5 14.5h-800q-21 0 -35.5 -14.5t-14.5 -35.5v-800z" />
+<glyph unicode="&#xe075;" d="M0 0v1100l500 -487v487l564 -550l-564 -550v488z" />
+<glyph unicode="&#xe076;" d="M0 0v1100l500 -487v487l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438l-500 -488v488z" />
+<glyph unicode="&#xe077;" d="M300 0v1100l500 -487v437q0 21 14.5 35.5t35.5 14.5h100q21 0 35.5 -14.5t14.5 -35.5v-1000q0 -21 -14.5 -35.5t-35.5 -14.5h-100q-21 0 -35.5 14.5t-14.5 35.5v438z" />
+<glyph unicode="&#xe078;" d="M100 250v100q0 21 14.5 35.5t35.5 14.5h1000q21 0 35.5 -14.5t14.5 -35.5v-100q0 -21 -14.5 -35.5t-35.5 -14.5h-1000q-21 0 -35.5 14.5t-14.5 35.5zM100 500h1100l-550 564z" />
+<glyph unicode="&#xe079;" d="M185 599l592 -592l240 240l-353 353l353 353l-240 240z" />
+<glyph unicode="&#xe080;" d="M272 194l353 353l-353 353l241 240l572 -571l21 -22l-1 -1v-1l-592 -591z" />
+<glyph unicode="&#xe081;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h200v-200h200v200h200v200h-200v200h-200v-200h-200v-200z" />
+<glyph unicode="&#xe082;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM300 500h600v200h-600v-200z" />
+<glyph unicode="&#xe083;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM246 459l213 -213l141 142l141 -142l213 213l-142 141l142 141l-213 212l-141 -141l-141 142l-212 -213l141 -141 z" />
+<glyph unicode="&#xe084;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM270 551l276 -277l411 411l-175 174l-236 -236l-102 102z" />
+<glyph unicode="&#xe085;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM364 700h143q4 0 11.5 -1t11 -1t6.5 3t3 9t1 11t3.5 8.5t3.5 6t5.5 4t6.5 2.5t9 1.5t9 0.5h11.5h12.5 q19 0 30 -10t11 -26q0 -22 -4 -28t-27 -22q-5 -1 -12.5 -3t-27 -13.5t-34 -27t-26.5 -46t-11 -68.5h200q5 3 14 8t31.5 25.5t39.5 45.5t31 69t14 94q0 51 -17.5 89t-42 58t-58.5 32t-58.5 15t-51.5 3q-50 0 -90.5 -12t-75 -38.5t-53.5 -74.5t-19 -114zM500 300h200v100h-200 v-100z" />
+<glyph unicode="&#xe086;" d="M3 600q0 162 80 299.5t217.5 217.5t299.5 80t299.5 -80t217.5 -217.5t80 -299.5t-80 -299.5t-217.5 -217.5t-299.5 -80t-299.5 80t-217.5 217.5t-80 299.5zM400 300h400v100h-100v300h-300v-100h100v-200h-100v-100zM500 800h200v100h-200v-100z" />
+<glyph unicode="&#xe087;" d="M0 500v200h195q31 125 98.5 199.5t206.5 100.5v200h200v-200q54 -20 113 -60t112.5 -105.5t71.5 -134.5h203v-200h-203q-25 -102 -116.5 -186t-180.5 -117v-197h-200v197q-140 27 -208 102.5t-98 200.5h-194zM290 500q24 -73 79.5 -127.5t130.5 -78.5v206h200v-206 q149 48 201 206h-201v200h200q-25 74 -75.5 127t-124.5 77v-204h-200v203q-75 -23 -130 -77t-79 -126h209v-200h-210z" />
+<glyph unicode="&#xe088;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM356 465l135 135 l-135 135l109 109l135 -135l135 135l109 -109l-135 -135l135 -135l-109 -109l-135 135l-135 -135z" />
+<glyph unicode="&#xe089;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM322 537l141 141 l87 -87l204 205l142 -142l-346 -345z" />
+<glyph unicode="&#xe090;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -115 62 -215l568 567q-100 62 -216 62q-171 0 -292.5 -121.5t-121.5 -292.5zM391 245q97 -59 209 -59q171 0 292.5 121.5t121.5 292.5 q0 112 -59 209z" />
+<glyph unicode="&#xe091;" d="M0 547l600 453v-300h600v-300h-600v-301z" />
+<glyph unicode="&#xe092;" d="M0 400v300h600v300l600 -453l-600 -448v301h-600z" />
+<glyph unicode="&#xe093;" d="M204 600l450 600l444 -600h-298v-600h-300v600h-296z" />
+<glyph unicode="&#xe094;" d="M104 600h296v600h300v-600h298l-449 -600z" />
+<glyph unicode="&#xe095;" d="M0 200q6 132 41 238.5t103.5 193t184 138t271.5 59.5v271l600 -453l-600 -448v301q-95 -2 -183 -20t-170 -52t-147 -92.5t-100 -135.5z" />
+<glyph unicode="&#xe096;" d="M0 0v400l129 -129l294 294l142 -142l-294 -294l129 -129h-400zM635 777l142 -142l294 294l129 -129v400h-400l129 -129z" />
+<glyph unicode="&#xe097;" d="M34 176l295 295l-129 129h400v-400l-129 130l-295 -295zM600 600v400l129 -129l295 295l142 -141l-295 -295l129 -130h-400z" />
+<glyph unicode="&#xe101;" d="M23 600q0 118 45.5 224.5t123 184t184 123t224.5 45.5t224.5 -45.5t184 -123t123 -184t45.5 -224.5t-45.5 -224.5t-123 -184t-184 -123t-224.5 -45.5t-224.5 45.5t-184 123t-123 184t-45.5 224.5zM456 851l58 -302q4 -20 21.5 -34.5t37.5 -14.5h54q20 0 37.5 14.5 t21.5 34.5l58 302q4 20 -8 34.5t-32 14.5h-207q-21 0 -33 -14.5t-8 -34.5zM500 300h200v100h-200v-100z" />
+<glyph unicode="&#xe102;" d="M0 800h100v-200h400v300h200v-300h400v200h100v100h-111q1 1 1 6.5t-1.5 15t-3.5 17.5l-34 172q-11 39 -41.5 63t-69.5 24q-32 0 -61 -17l-239 -144q-22 -13 -40 -35q-19 24 -40 36l-238 144q-33 18 -62 18q-39 0 -69.5 -23t-40.5 -61l-35 -177q-2 -8 -3 -18t-1 -15v-6 h-111v-100zM100 0h400v400h-400v-400zM200 900q-3 0 14 48t36 96l18 47l213 -191h-281zM700 0v400h400v-400h-400zM731 900l202 197q5 -12 12 -32.5t23 -64t25 -72t7 -28.5h-269z" />
+<glyph unicode="&#xe103;" d="M0 -22v143l216 193q-9 53 -13 83t-5.5 94t9 113t38.5 114t74 124q47 60 99.5 102.5t103 68t127.5 48t145.5 37.5t184.5 43.5t220 58.5q0 -189 -22 -343t-59 -258t-89 -181.5t-108.5 -120t-122 -68t-125.5 -30t-121.5 -1.5t-107.5 12.5t-87.5 17t-56.5 7.5l-99 -55z M238.5 300.5q19.5 -6.5 86.5 76.5q55 66 367 234q70 38 118.5 69.5t102 79t99 111.5t86.5 148q22 50 24 60t-6 19q-7 5 -17 5t-26.5 -14.5t-33.5 -39.5q-35 -51 -113.5 -108.5t-139.5 -89.5l-61 -32q-369 -197 -458 -401q-48 -111 -28.5 -117.5z" />
+<glyph unicode="&#xe104;" d="M111 408q0 -33 5 -63q9 -56 44 -119.5t105 -108.5q31 -21 64 -16t62 23.5t57 49.5t48 61.5t35 60.5q32 66 39 184.5t-13 157.5q79 -80 122 -164t26 -184q-5 -33 -20.5 -69.5t-37.5 -80.5q-10 -19 -14.5 -29t-12 -26t-9 -23.5t-3 -19t2.5 -15.5t11 -9.5t19.5 -5t30.5 2.5 t42 8q57 20 91 34t87.5 44.5t87 64t65.5 88.5t47 122q38 172 -44.5 341.5t-246.5 278.5q22 -44 43 -129q39 -159 -32 -154q-15 2 -33 9q-79 33 -120.5 100t-44 175.5t48.5 257.5q-13 -8 -34 -23.5t-72.5 -66.5t-88.5 -105.5t-60 -138t-8 -166.5q2 -12 8 -41.5t8 -43t6 -39.5 t3.5 -39.5t-1 -33.5t-6 -31.5t-13.5 -24t-21 -20.5t-31 -12q-38 -10 -67 13t-40.5 61.5t-15 81.5t10.5 75q-52 -46 -83.5 -101t-39 -107t-7.5 -85z" />
+<glyph unicode="&#xe105;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5t145.5 -23.5t132.5 -59t116.5 -83.5t97 -90t74.5 -85.5t49 -63.5t20 -30l26 -40l-26 -40q-6 -10 -20 -30t-49 -63.5t-74.5 -85.5t-97 -90t-116.5 -83.5t-132.5 -59t-145.5 -23.5 t-145.5 23.5t-132.5 59t-116.5 83.5t-97 90t-74.5 85.5t-49 63.5t-20 30zM120 600q7 -10 40.5 -58t56 -78.5t68 -77.5t87.5 -75t103 -49.5t125 -21.5t123.5 20t100.5 45.5t85.5 71.5t66.5 75.5t58 81.5t47 66q-1 1 -28.5 37.5t-42 55t-43.5 53t-57.5 63.5t-58.5 54 q49 -74 49 -163q0 -124 -88 -212t-212 -88t-212 88t-88 212q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l105 105q-37 24 -75 72t-57 84l-20 36z" />
+<glyph unicode="&#xe106;" d="M-61 600l26 40q6 10 20 30t49 63.5t74.5 85.5t97 90t116.5 83.5t132.5 59t145.5 23.5q61 0 121 -17l37 142h148l-314 -1200h-148l37 143q-82 21 -165 71.5t-140 102t-109.5 112t-72 88.5t-29.5 43zM120 600q210 -282 393 -336l37 141q-107 18 -178.5 101.5t-71.5 193.5 q0 85 46 158q-102 -87 -226 -258zM377 656q49 -124 154 -191l47 47l23 87q-30 28 -59 69t-44 68l-14 26zM780 161l38 145q22 15 44.5 34t46 44t40.5 44t41 50.5t33.5 43.5t33 44t24.5 34q-97 127 -140 175l39 146q67 -54 131.5 -125.5t87.5 -103.5t36 -52l26 -40l-26 -40 q-7 -12 -25.5 -38t-63.5 -79.5t-95.5 -102.5t-124 -100t-146.5 -79z" />
+<glyph unicode="&#xe107;" d="M-97.5 34q13.5 -34 50.5 -34h1294q37 0 50.5 35.5t-7.5 67.5l-642 1056q-20 34 -48 36.5t-48 -29.5l-642 -1066q-21 -32 -7.5 -66zM155 200l445 723l445 -723h-345v100h-200v-100h-345zM500 600l100 -300l100 300v100h-200v-100z" />
+<glyph unicode="&#xe108;" d="M100 262v41q0 20 11 44.5t26 38.5l363 325v339q0 62 44 106t106 44t106 -44t44 -106v-339l363 -325q15 -14 26 -38.5t11 -44.5v-41q0 -20 -12 -26.5t-29 5.5l-359 249v-263q100 -91 100 -113v-64q0 -20 -13 -28.5t-32 0.5l-94 78h-222l-94 -78q-19 -9 -32 -0.5t-13 28.5 v64q0 22 100 113v263l-359 -249q-17 -12 -29 -5.5t-12 26.5z" />
+<glyph unicode="&#xe109;" d="M0 50q0 -20 14.5 -35t35.5 -15h1000q21 0 35.5 15t14.5 35v750h-1100v-750zM0 900h1100v150q0 21 -14.5 35.5t-35.5 14.5h-150v100h-100v-100h-500v100h-100v-100h-150q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 100v100h100v-100h-100zM100 300v100h100v-100h-100z M100 500v100h100v-100h-100zM300 100v100h100v-100h-100zM300 300v100h100v-100h-100zM300 500v100h100v-100h-100zM500 100v100h100v-100h-100zM500 300v100h100v-100h-100zM500 500v100h100v-100h-100zM700 100v100h100v-100h-100zM700 300v100h100v-100h-100zM700 500 v100h100v-100h-100zM900 100v100h100v-100h-100zM900 300v100h100v-100h-100zM900 500v100h100v-100h-100z" />
+<glyph unicode="&#xe110;" d="M0 200v200h259l600 600h241v198l300 -295l-300 -300v197h-159l-600 -600h-341zM0 800h259l122 -122l141 142l-181 180h-341v-200zM678 381l141 142l122 -123h159v198l300 -295l-300 -300v197h-241z" />
+<glyph unicode="&#xe111;" d="M0 400v600q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-600q0 -41 -29.5 -70.5t-70.5 -29.5h-596l-304 -300v300h-100q-41 0 -70.5 29.5t-29.5 70.5z" />
+<glyph unicode="&#xe112;" d="M100 600v200h300v-250q0 -113 6 -145q17 -92 102 -117q39 -11 92 -11q37 0 66.5 5.5t50 15.5t36 24t24 31.5t14 37.5t7 42t2.5 45t0 47v25v250h300v-200q0 -42 -3 -83t-15 -104t-31.5 -116t-58 -109.5t-89 -96.5t-129 -65.5t-174.5 -25.5t-174.5 25.5t-129 65.5t-89 96.5 t-58 109.5t-31.5 116t-15 104t-3 83zM100 900v300h300v-300h-300zM800 900v300h300v-300h-300z" />
+<glyph unicode="&#xe113;" d="M-30 411l227 -227l352 353l353 -353l226 227l-578 579z" />
+<glyph unicode="&#xe114;" d="M70 797l580 -579l578 579l-226 227l-353 -353l-352 353z" />
+<glyph unicode="&#xe115;" d="M-198 700l299 283l300 -283h-203v-400h385l215 -200h-800v600h-196zM402 1000l215 -200h381v-400h-198l299 -283l299 283h-200v600h-796z" />
+<glyph unicode="&#xe116;" d="M18 939q-5 24 10 42q14 19 39 19h896l38 162q5 17 18.5 27.5t30.5 10.5h94q20 0 35 -14.5t15 -35.5t-15 -35.5t-35 -14.5h-54l-201 -961q-2 -4 -6 -10.5t-19 -17.5t-33 -11h-31v-50q0 -20 -14.5 -35t-35.5 -15t-35.5 15t-14.5 35v50h-300v-50q0 -20 -14.5 -35t-35.5 -15 t-35.5 15t-14.5 35v50h-50q-21 0 -35.5 15t-14.5 35q0 21 14.5 35.5t35.5 14.5h535l48 200h-633q-32 0 -54.5 21t-27.5 43z" />
+<glyph unicode="&#xe117;" d="M0 0v800h1200v-800h-1200zM0 900v100h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-100h-1200z" />
+<glyph unicode="&#xe118;" d="M1 0l300 700h1200l-300 -700h-1200zM1 400v600h200q0 41 29.5 70.5t70.5 29.5h300q41 0 70.5 -29.5t29.5 -70.5h500v-200h-1000z" />
+<glyph unicode="&#xe119;" d="M302 300h198v600h-198l298 300l298 -300h-198v-600h198l-298 -300z" />
+<glyph unicode="&#xe120;" d="M0 600l300 298v-198h600v198l300 -298l-300 -297v197h-600v-197z" />
+<glyph unicode="&#xe121;" d="M0 100v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM31 400l172 739q5 22 23 41.5t38 19.5h672q19 0 37.5 -22.5t23.5 -45.5l172 -732h-1138zM800 100h100v100h-100v-100z M1000 100h100v100h-100v-100z" />
+<glyph unicode="&#xe122;" d="M-101 600v50q0 24 25 49t50 38l25 13v-250l-11 5.5t-24 14t-30 21.5t-24 27.5t-11 31.5zM100 500v250v8v8v7t0.5 7t1.5 5.5t2 5t3 4t4.5 3.5t6 1.5t7.5 0.5h200l675 250v-850l-675 200h-38l47 -276q2 -12 -3 -17.5t-11 -6t-21 -0.5h-8h-83q-20 0 -34.5 14t-18.5 35 q-55 337 -55 351zM1100 200v850q0 21 14.5 35.5t35.5 14.5q20 0 35 -14.5t15 -35.5v-850q0 -20 -15 -35t-35 -15q-21 0 -35.5 15t-14.5 35z" />
+<glyph unicode="&#xe123;" d="M74 350q0 21 13.5 35.5t33.5 14.5h18l117 173l63 327q15 77 76 140t144 83l-18 32q-6 19 3 32t29 13h94q20 0 29 -10.5t3 -29.5q-18 -36 -18 -37q83 -19 144 -82.5t76 -140.5l63 -327l118 -173h17q20 0 33.5 -14.5t13.5 -35.5q0 -20 -13 -40t-31 -27q-8 -3 -23 -8.5 t-65 -20t-103 -25t-132.5 -19.5t-158.5 -9q-125 0 -245.5 20.5t-178.5 40.5l-58 20q-18 7 -31 27.5t-13 40.5zM497 110q12 -49 40 -79.5t63 -30.5t63 30.5t39 79.5q-48 -6 -102 -6t-103 6z" />
+<glyph unicode="&#xe124;" d="M21 445l233 -45l-78 -224l224 78l45 -233l155 179l155 -179l45 233l224 -78l-78 224l234 45l-180 155l180 156l-234 44l78 225l-224 -78l-45 233l-155 -180l-155 180l-45 -233l-224 78l78 -225l-233 -44l179 -156z" />
+<glyph unicode="&#xe125;" d="M0 200h200v600h-200v-600zM300 275q0 -75 100 -75h61q124 -100 139 -100h250q46 0 83 57l238 344q29 31 29 74v100q0 44 -30.5 84.5t-69.5 40.5h-328q28 118 28 125v150q0 44 -30.5 84.5t-69.5 40.5h-50q-27 0 -51 -20t-38 -48l-96 -198l-145 -196q-20 -26 -20 -63v-400z M400 300v375l150 213l100 212h50v-175l-50 -225h450v-125l-250 -375h-214l-136 100h-100z" />
+<glyph unicode="&#xe126;" d="M0 400v600h200v-600h-200zM300 525v400q0 75 100 75h61q124 100 139 100h250q46 0 83 -57l238 -344q29 -31 29 -74v-100q0 -44 -30.5 -84.5t-69.5 -40.5h-328q28 -118 28 -125v-150q0 -44 -30.5 -84.5t-69.5 -40.5h-50q-27 0 -51 20t-38 48l-96 198l-145 196 q-20 26 -20 63zM400 525l150 -212l100 -213h50v175l-50 225h450v125l-250 375h-214l-136 -100h-100v-375z" />
+<glyph unicode="&#xe127;" d="M8 200v600h200v-600h-200zM308 275v525q0 17 14 35.5t28 28.5l14 9l362 230q14 6 25 6q17 0 29 -12l109 -112q14 -14 14 -34q0 -18 -11 -32l-85 -121h302q85 0 138.5 -38t53.5 -110t-54.5 -111t-138.5 -39h-107l-130 -339q-7 -22 -20.5 -41.5t-28.5 -19.5h-341 q-7 0 -90 81t-83 94zM408 289l100 -89h293l131 339q6 21 19.5 41t28.5 20h203q16 0 25 15t9 36q0 20 -9 34.5t-25 14.5h-457h-6.5h-7.5t-6.5 0.5t-6 1t-5 1.5t-5.5 2.5t-4 4t-4 5.5q-5 12 -5 20q0 14 10 27l147 183l-86 83l-339 -236v-503z" />
+<glyph unicode="&#xe128;" d="M-101 651q0 72 54 110t139 38l302 -1l-85 121q-11 16 -11 32q0 21 14 34l109 113q13 12 29 12q11 0 25 -6l365 -230q7 -4 17 -10.5t26.5 -26t16.5 -36.5v-526q0 -13 -86 -93.5t-94 -80.5h-341q-16 0 -29.5 20t-19.5 41l-130 339h-107q-84 0 -139 39t-55 111zM-1 601h222 q15 0 28.5 -20.5t19.5 -40.5l131 -339h293l107 89v502l-343 237l-87 -83l145 -184q10 -11 10 -26q0 -11 -5 -20q-1 -3 -3.5 -5.5l-4 -4t-5 -2.5t-5.5 -1.5t-6.5 -1t-6.5 -0.5h-7.5h-6.5h-476v-100zM1000 201v600h200v-600h-200z" />
+<glyph unicode="&#xe129;" d="M97 719l230 -363q4 -6 10.5 -15.5t26 -25t36.5 -15.5h525q13 0 94 83t81 90v342q0 15 -20 28.5t-41 19.5l-339 131v106q0 84 -39 139t-111 55t-110 -53.5t-38 -138.5v-302l-121 84q-15 12 -33.5 11.5t-32.5 -13.5l-112 -110q-22 -22 -6 -53zM172 739l83 86l183 -146 q22 -18 47 -5q3 1 5.5 3.5l4 4t2.5 5t1.5 5.5t1 6.5t0.5 6.5v7.5v6.5v456q0 22 25 31t50 -0.5t25 -30.5v-202q0 -16 20 -29.5t41 -19.5l339 -130v-294l-89 -100h-503zM400 0v200h600v-200h-600z" />
+<glyph unicode="&#xe130;" d="M2 585q-16 -31 6 -53l112 -110q13 -13 32 -13.5t34 10.5l121 85q0 -51 -0.5 -153.5t-0.5 -148.5q0 -84 38.5 -138t110.5 -54t111 55t39 139v106l339 131q20 6 40.5 19.5t20.5 28.5v342q0 7 -81 90t-94 83h-525q-17 0 -35.5 -14t-28.5 -28l-10 -15zM77 565l236 339h503 l89 -100v-294l-340 -130q-20 -6 -40 -20t-20 -29v-202q0 -22 -25 -31t-50 0t-25 31v456v14.5t-1.5 11.5t-5 12t-9.5 7q-24 13 -46 -5l-184 -146zM305 1104v200h600v-200h-600z" />
+<glyph unicode="&#xe131;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM298 701l2 -201h300l-2 -194l402 294l-402 298v-197h-300z" />
+<glyph unicode="&#xe132;" d="M0 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t231.5 47.5q122 0 232.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-218 -217.5t-300 -80t-299.5 80t-217.5 217.5t-80 299.5zM200 600l402 -294l-2 194h300l2 201h-300v197z" />
+<glyph unicode="&#xe133;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600h200v-300h200v300h200l-300 400z" />
+<glyph unicode="&#xe134;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q162 0 299.5 -80t217.5 -218t80 -300t-80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM300 600l300 -400l300 400h-200v300h-200v-300h-200z" />
+<glyph unicode="&#xe135;" d="M5 597q0 122 47.5 232.5t127.5 190.5t190.5 127.5t232.5 47.5q121 0 231.5 -47.5t190.5 -127.5t127.5 -190.5t47.5 -232.5q0 -162 -80 -299.5t-217.5 -217.5t-299.5 -80t-300 80t-218 217.5t-80 299.5zM254 780q-8 -33 5.5 -92.5t7.5 -87.5q0 -9 17 -44t16 -60 q12 0 23 -5.5t23 -15t20 -13.5q24 -12 108 -42q22 -8 53 -31.5t59.5 -38.5t57.5 -11q8 -18 -15 -55t-20 -57q42 -71 87 -80q0 -6 -3 -15.5t-3.5 -14.5t4.5 -17q104 -3 221 112q30 29 47 47t34.5 49t20.5 62q-14 9 -37 9.5t-36 7.5q-14 7 -49 15t-52 19q-9 0 -39.5 -0.5 t-46.5 -1.5t-39 -6.5t-39 -16.5q-50 -35 -66 -12q-4 2 -3.5 25.5t0.5 25.5q-6 13 -26.5 17t-24.5 7q2 22 -2 41t-16.5 28t-38.5 -20q-23 -25 -42 4q-19 28 -8 58q6 16 22 22q6 -1 26 -1.5t33.5 -4t19.5 -13.5q12 -19 32 -37.5t34 -27.5l14 -8q0 3 9.5 39.5t5.5 57.5 q-4 23 14.5 44.5t22.5 31.5q5 14 10 35t8.5 31t15.5 22.5t34 21.5q-6 18 10 37q8 0 23.5 -1.5t24.5 -1.5t20.5 4.5t20.5 15.5q-10 23 -30.5 42.5t-38 30t-49 26.5t-43.5 23q11 39 2 44q31 -13 58 -14.5t39 3.5l11 4q7 36 -16.5 53.5t-64.5 28.5t-56 23q-19 -3 -37 0 q-15 -12 -36.5 -21t-34.5 -12t-44 -8t-39 -6q-15 -3 -45.5 0.5t-45.5 -2.5q-21 -7 -52 -26.5t-34 -34.5q-3 -11 6.5 -22.5t8.5 -18.5q-3 -34 -27.5 -90.5t-29.5 -79.5zM518 916q3 12 16 30t16 25q10 -10 18.5 -10t14 6t14.5 14.5t16 12.5q0 -24 17 -66.5t17 -43.5 q-9 2 -31 5t-36 5t-32 8t-30 14zM692 1003h1h-1z" />
+<glyph unicode="&#xe136;" d="M0 164.5q0 21.5 15 37.5l600 599q-33 101 6 201.5t135 154.5q164 92 306 -9l-259 -138l145 -232l251 126q13 -175 -151 -267q-123 -70 -253 -23l-596 -596q-15 -16 -36.5 -16t-36.5 16l-111 110q-15 15 -15 36.5z" />
+<glyph unicode="&#xe137;" horiz-adv-x="1220" d="M0 196v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 596v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000 q-41 0 -70.5 29.5t-29.5 70.5zM0 996v100q0 41 29.5 70.5t70.5 29.5h1000q41 0 70.5 -29.5t29.5 -70.5v-100q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM600 596h500v100h-500v-100zM800 196h300v100h-300v-100zM900 996h200v100h-200v-100z" />
+<glyph unicode="&#xe138;" d="M100 1100v100h1000v-100h-1000zM150 1000h900l-350 -500v-300l-200 -200v500z" />
+<glyph unicode="&#xe139;" d="M0 200v200h1200v-200q0 -41 -29.5 -70.5t-70.5 -29.5h-1000q-41 0 -70.5 29.5t-29.5 70.5zM0 500v400q0 41 29.5 70.5t70.5 29.5h300v100q0 41 29.5 70.5t70.5 29.5h200q41 0 70.5 -29.5t29.5 -70.5v-100h300q41 0 70.5 -29.5t29.5 -70.5v-400h-500v100h-200v-100h-500z M500 1000h200v100h-200v-100z" />
+<glyph unicode="&#xe140;" d="M0 0v400l129 -129l200 200l142 -142l-200 -200l129 -129h-400zM0 800l129 129l200 -200l142 142l-200 200l129 129h-400v-400zM729 329l142 142l200 -200l129 129v-400h-400l129 129zM729 871l200 200l-129 129h400v-400l-129 129l-200 -200z" />
+<glyph unicode="&#xe141;" d="M0 596q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 596q0 -172 121.5 -293t292.5 -121t292.5 121t121.5 293q0 171 -121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM291 655 q0 23 15.5 38.5t38.5 15.5t39 -16t16 -38q0 -23 -16 -39t-39 -16q-22 0 -38 16t-16 39zM400 850q0 22 16 38.5t39 16.5q22 0 38 -16t16 -39t-16 -39t-38 -16q-23 0 -39 16.5t-16 38.5zM514 609q0 32 20.5 56.5t51.5 29.5l122 126l1 1q-9 14 -9 28q0 22 16 38.5t39 16.5 q22 0 38 -16t16 -39t-16 -39t-38 -16q-14 0 -29 10l-55 -145q17 -22 17 -51q0 -36 -25.5 -61.5t-61.5 -25.5t-61.5 25.5t-25.5 61.5zM800 655q0 22 16 38t39 16t38.5 -15.5t15.5 -38.5t-16 -39t-38 -16q-23 0 -39 16t-16 39z" />
+<glyph unicode="&#xe142;" d="M-40 375q-13 -95 35 -173q35 -57 94 -89t129 -32q63 0 119 28q33 16 65 40.5t52.5 45.5t59.5 64q40 44 57 61l394 394q35 35 47 84t-3 96q-27 87 -117 104q-20 2 -29 2q-46 0 -78.5 -16.5t-67.5 -51.5l-389 -396l-7 -7l69 -67l377 373q20 22 39 38q23 23 50 23 q38 0 53 -36q16 -39 -20 -75l-547 -547q-52 -52 -125 -52q-55 0 -100 33t-54 96q-5 35 2.5 66t31.5 63t42 50t56 54q24 21 44 41l348 348q52 52 82.5 79.5t84 54t107.5 26.5q25 0 48 -4q95 -17 154 -94.5t51 -175.5q-7 -101 -98 -192l-252 -249l-253 -256l7 -7l69 -60 l517 511q67 67 95 157t11 183q-16 87 -67 154t-130 103q-69 33 -152 33q-107 0 -197 -55q-40 -24 -111 -95l-512 -512q-68 -68 -81 -163z" />
+<glyph unicode="&#xe143;" d="M80 784q0 131 98.5 229.5t230.5 98.5q143 0 241 -129q103 129 246 129q129 0 226 -98.5t97 -229.5q0 -46 -17.5 -91t-61 -99t-77 -89.5t-104.5 -105.5q-197 -191 -293 -322l-17 -23l-16 23q-43 58 -100 122.5t-92 99.5t-101 100q-71 70 -104.5 105.5t-77 89.5t-61 99 t-17.5 91zM250 784q0 -27 30.5 -70t61.5 -75.5t95 -94.5l22 -22q93 -90 190 -201q82 92 195 203l12 12q64 62 97.5 97t64.5 79t31 72q0 71 -48 119.5t-105 48.5q-74 0 -132 -83l-118 -171l-114 174q-51 80 -123 80q-60 0 -109.5 -49.5t-49.5 -118.5z" />
+<glyph unicode="&#xe144;" d="M57 353q0 -95 66 -159l141 -142q68 -66 159 -66q93 0 159 66l283 283q66 66 66 159t-66 159l-141 141q-8 9 -19 17l-105 -105l212 -212l-389 -389l-247 248l95 95l-18 18q-46 45 -75 101l-55 -55q-66 -66 -66 -159zM269 706q0 -93 66 -159l141 -141q7 -7 19 -17l105 105 l-212 212l389 389l247 -247l-95 -96l18 -17q47 -49 77 -100l29 29q35 35 62.5 88t27.5 96q0 93 -66 159l-141 141q-66 66 -159 66q-95 0 -159 -66l-283 -283q-66 -64 -66 -159z" />
+<glyph unicode="&#xe145;" d="M200 100v953q0 21 30 46t81 48t129 38t163 15t162 -15t127 -38t79 -48t29 -46v-953q0 -41 -29.5 -70.5t-70.5 -29.5h-600q-41 0 -70.5 29.5t-29.5 70.5zM300 300h600v700h-600v-700zM496 150q0 -43 30.5 -73.5t73.5 -30.5t73.5 30.5t30.5 73.5t-30.5 73.5t-73.5 30.5 t-73.5 -30.5t-30.5 -73.5z" />
+<glyph unicode="&#xe146;" d="M0 0l303 380l207 208l-210 212h300l267 279l-35 36q-15 14 -15 35t15 35q14 15 35 15t35 -15l283 -282q15 -15 15 -36t-15 -35q-14 -15 -35 -15t-35 15l-36 35l-279 -267v-300l-212 210l-208 -207z" />
+<glyph unicode="&#xe148;" d="M295 433h139q5 -77 48.5 -126.5t117.5 -64.5v335q-6 1 -15.5 4t-11.5 3q-46 14 -79 26.5t-72 36t-62.5 52t-40 72.5t-16.5 99q0 92 44 159.5t109 101t144 40.5v78h100v-79q38 -4 72.5 -13.5t75.5 -31.5t71 -53.5t51.5 -84t24.5 -118.5h-159q-8 72 -35 109.5t-101 50.5 v-307l64 -14q34 -7 64 -16.5t70 -31.5t67.5 -52t47.5 -80.5t20 -112.5q0 -139 -89 -224t-244 -96v-77h-100v78q-152 17 -237 104q-40 40 -52.5 93.5t-15.5 139.5zM466 889q0 -29 8 -51t16.5 -34t29.5 -22.5t31 -13.5t38 -10q7 -2 11 -3v274q-61 -8 -97.5 -37.5t-36.5 -102.5 zM700 237q170 18 170 151q0 64 -44 99.5t-126 60.5v-311z" />
+<glyph unicode="&#xe149;" d="M100 600v100h166q-24 49 -44 104q-10 26 -14.5 55.5t-3 72.5t25 90t68.5 87q97 88 263 88q129 0 230 -89t101 -208h-153q0 52 -34 89.5t-74 51.5t-76 14q-37 0 -79 -14.5t-62 -35.5q-41 -44 -41 -101q0 -28 16.5 -69.5t28 -62.5t41.5 -72h241v-100h-197q8 -50 -2.5 -115 t-31.5 -94q-41 -59 -99 -113q35 11 84 18t70 7q33 1 103 -16t103 -17q76 0 136 30l50 -147q-41 -25 -80.5 -36.5t-59 -13t-61.5 -1.5q-23 0 -128 33t-155 29q-39 -4 -82 -17t-66 -25l-24 -11l-55 145l16.5 11t15.5 10t13.5 9.5t14.5 12t14.5 14t17.5 18.5q48 55 54 126.5 t-30 142.5h-221z" />
+<glyph unicode="&#xe150;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM602 900l298 300l298 -300h-198v-900h-200v900h-198z" />
+<glyph unicode="&#xe151;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v200h100v-100h200v-100h-300zM700 400v100h300v-200h-99v-100h-100v100h99v100h-200zM700 700v500h300v-500h-100v100h-100v-100h-100zM801 900h100v200h-100v-200z" />
+<glyph unicode="&#xe152;" d="M2 300h198v900h200v-900h198l-298 -300zM700 0v500h300v-500h-100v100h-100v-100h-100zM700 700v200h100v-100h200v-100h-300zM700 1100v100h300v-200h-99v-100h-100v100h99v100h-200zM801 200h100v200h-100v-200z" />
+<glyph unicode="&#xe153;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 100v400h300v-500h-100v100h-200zM800 1100v100h200v-500h-100v400h-100zM901 200h100v200h-100v-200z" />
+<glyph unicode="&#xe154;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM800 400v100h200v-500h-100v400h-100zM800 800v400h300v-500h-100v100h-200zM901 900h100v200h-100v-200z" />
+<glyph unicode="&#xe155;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h500v-200h-500zM700 400v200h400v-200h-400zM700 700v200h300v-200h-300zM700 1000v200h200v-200h-200z" />
+<glyph unicode="&#xe156;" d="M2 300l298 -300l298 300h-198v900h-200v-900h-198zM700 100v200h200v-200h-200zM700 400v200h300v-200h-300zM700 700v200h400v-200h-400zM700 1000v200h500v-200h-500z" />
+<glyph unicode="&#xe157;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q162 0 281 -118.5t119 -281.5v-300q0 -165 -118.5 -282.5t-281.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500z" />
+<glyph unicode="&#xe158;" d="M0 400v300q0 163 119 281.5t281 118.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-163 0 -281.5 117.5t-118.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM400 300l333 250l-333 250v-500z" />
+<glyph unicode="&#xe159;" d="M0 400v300q0 163 117.5 281.5t282.5 118.5h300q163 0 281.5 -119t118.5 -281v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-300q-165 0 -282.5 117.5t-117.5 282.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 700l250 -333l250 333h-500z" />
+<glyph unicode="&#xe160;" d="M0 400v300q0 165 117.5 282.5t282.5 117.5h300q165 0 282.5 -117.5t117.5 -282.5v-300q0 -162 -118.5 -281t-281.5 -119h-300q-165 0 -282.5 118.5t-117.5 281.5zM200 300q0 -41 29.5 -70.5t70.5 -29.5h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5 h-500q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM300 400h500l-250 333z" />
+<glyph unicode="&#xe161;" d="M0 400v300h300v200l400 -350l-400 -350v200h-300zM500 0v200h500q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-500v200h400q165 0 282.5 -117.5t117.5 -282.5v-300q0 -165 -117.5 -282.5t-282.5 -117.5h-400z" />
+<glyph unicode="&#xe162;" d="M217 519q8 -19 31 -19h302q-155 -438 -160 -458q-5 -21 4 -32l9 -8h9q14 0 26 15q11 13 274.5 321.5t264.5 308.5q14 19 5 36q-8 17 -31 17l-301 -1q1 4 78 219.5t79 227.5q2 15 -5 27l-9 9h-9q-15 0 -25 -16q-4 -6 -98 -111.5t-228.5 -257t-209.5 -237.5q-16 -19 -6 -41 z" />
+<glyph unicode="&#xe163;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q47 0 100 15v185h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h500v185q-14 4 -114 7.5t-193 5.5l-93 2q-165 0 -282.5 -117.5t-117.5 -282.5v-300zM600 400v300h300v200l400 -350l-400 -350v200h-300z " />
+<glyph unicode="&#xe164;" d="M0 400q0 -165 117.5 -282.5t282.5 -117.5h300q163 0 281.5 117.5t118.5 282.5v98l-78 73l-122 -123v-148q0 -41 -29.5 -70.5t-70.5 -29.5h-500q-41 0 -70.5 29.5t-29.5 70.5v500q0 41 29.5 70.5t70.5 29.5h156l118 122l-74 78h-100q-165 0 -282.5 -117.5t-117.5 -282.5 v-300zM496 709l353 342l-149 149h500v-500l-149 149l-342 -353z" />
+<glyph unicode="&#xe165;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM406 600 q0 80 57 137t137 57t137 -57t57 -137t-57 -137t-137 -57t-137 57t-57 137z" />
+<glyph unicode="&#xe166;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 800l445 -500l450 500h-295v400h-300v-400h-300zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe167;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 700h300v-300h300v300h295l-445 500zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe168;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 705l305 -305l596 596l-154 155l-442 -442l-150 151zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe169;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM100 988l97 -98l212 213l-97 97zM200 400l697 1l3 699l-250 -239l-149 149l-212 -212l149 -149zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe170;" d="M0 0v275q0 11 7 18t18 7h1048q11 0 19 -7.5t8 -17.5v-275h-1100zM200 612l212 -212l98 97l-213 212zM300 1200l239 -250l-149 -149l212 -212l149 148l249 -237l-1 697zM900 150h100v50h-100v-50z" />
+<glyph unicode="&#xe171;" d="M23 415l1177 784v-1079l-475 272l-310 -393v416h-392zM494 210l672 938l-672 -712v-226z" />
+<glyph unicode="&#xe172;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-850q0 -21 -15 -35.5t-35 -14.5h-150v400h-700v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe173;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-218l-276 -275l-120 120l-126 -127h-378v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM581 306l123 123l120 -120l353 352l123 -123l-475 -476zM600 1000h100v200h-100v-200z" />
+<glyph unicode="&#xe174;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-269l-103 -103l-170 170l-298 -298h-329v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 1000h100v200h-100v-200zM700 133l170 170l-170 170l127 127l170 -170l170 170l127 -128l-170 -169l170 -170 l-127 -127l-170 170l-170 -170z" />
+<glyph unicode="&#xe175;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-300h-400v-200h-500v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300l300 -300l300 300h-200v300h-200v-300h-200zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe176;" d="M0 150v1000q0 20 14.5 35t35.5 15h250v-300h500v300h100l200 -200v-402l-200 200l-298 -298h-402v-400h-150q-21 0 -35.5 14.5t-14.5 35.5zM600 300h200v-300h200v300h200l-300 300zM600 1000v200h100v-200h-100z" />
+<glyph unicode="&#xe177;" d="M0 250q0 -21 14.5 -35.5t35.5 -14.5h1100q21 0 35.5 14.5t14.5 35.5v550h-1200v-550zM0 900h1200v150q0 21 -14.5 35.5t-35.5 14.5h-1100q-21 0 -35.5 -14.5t-14.5 -35.5v-150zM100 300v200h400v-200h-400z" />
+<glyph unicode="&#xe178;" d="M0 400l300 298v-198h400v-200h-400v-198zM100 800v200h100v-200h-100zM300 800v200h100v-200h-100zM500 800v200h400v198l300 -298l-300 -298v198h-400zM800 300v200h100v-200h-100zM1000 300h100v200h-100v-200z" />
+<glyph unicode="&#xe179;" d="M100 700v400l50 100l50 -100v-300h100v300l50 100l50 -100v-300h100v300l50 100l50 -100v-400l-100 -203v-447q0 -21 -14.5 -35.5t-35.5 -14.5h-200q-21 0 -35.5 14.5t-14.5 35.5v447zM800 597q0 -29 10.5 -55.5t25 -43t29 -28.5t25.5 -18l10 -5v-397q0 -21 14.5 -35.5 t35.5 -14.5h200q21 0 35.5 14.5t14.5 35.5v1106q0 31 -18 40.5t-44 -7.5l-276 -116q-25 -17 -43.5 -51.5t-18.5 -65.5v-359z" />
+<glyph unicode="&#xe180;" d="M100 0h400v56q-75 0 -87.5 6t-12.5 44v394h500v-394q0 -38 -12.5 -44t-87.5 -6v-56h400v56q-4 0 -11 0.5t-24 3t-30 7t-24 15t-11 24.5v888q0 22 25 34.5t50 13.5l25 2v56h-400v-56q75 0 87.5 -6t12.5 -44v-394h-500v394q0 38 12.5 44t87.5 6v56h-400v-56q4 0 11 -0.5 t24 -3t30 -7t24 -15t11 -24.5v-888q0 -22 -25 -34.5t-50 -13.5l-25 -2v-56z" />
+<glyph unicode="&#xe181;" d="M0 300q0 -41 29.5 -70.5t70.5 -29.5h300q41 0 70.5 29.5t29.5 70.5v500q0 41 -29.5 70.5t-70.5 29.5h-300q-41 0 -70.5 -29.5t-29.5 -70.5v-500zM100 100h400l200 200h105l295 98v-298h-425l-100 -100h-375zM100 300v200h300v-200h-300zM100 600v200h300v-200h-300z M100 1000h400l200 -200v-98l295 98h105v200h-425l-100 100h-375zM700 402v163l400 133v-163z" />
+<glyph unicode="&#xe182;" d="M16.5 974.5q0.5 -21.5 16 -90t46.5 -140t104 -177.5t175 -208q103 -103 207.5 -176t180 -103.5t137 -47t92.5 -16.5l31 1l163 162q17 18 13.5 41t-22.5 37l-192 136q-19 14 -45 12t-42 -19l-118 -118q-142 101 -268 227t-227 268l118 118q17 17 20 41.5t-11 44.5 l-139 194q-14 19 -36.5 22t-40.5 -14l-162 -162q-1 -11 -0.5 -32.5z" />
+<glyph unicode="&#xe183;" d="M0 50v212q0 20 10.5 45.5t24.5 39.5l365 303v50q0 4 1 10.5t12 22.5t30 28.5t60 23t97 10.5t97 -10t60 -23.5t30 -27.5t12 -24l1 -10v-50l365 -303q14 -14 24.5 -39.5t10.5 -45.5v-212q0 -21 -14.5 -35.5t-35.5 -14.5h-1100q-20 0 -35 14.5t-15 35.5zM0 712 q0 -21 14.5 -33.5t34.5 -8.5l202 33q20 4 34.5 21t14.5 38v146q141 24 300 24t300 -24v-146q0 -21 14.5 -38t34.5 -21l202 -33q20 -4 34.5 8.5t14.5 33.5v200q-6 8 -19 20.5t-63 45t-112 57t-171 45t-235 20.5q-92 0 -175 -10.5t-141.5 -27t-108.5 -36.5t-81.5 -40 t-53.5 -36.5t-31 -27.5l-9 -10v-200z" />
+<glyph unicode="&#xe184;" d="M100 0v100h1100v-100h-1100zM175 200h950l-125 150v250l100 100v400h-100v-200h-100v200h-200v-200h-100v200h-200v-200h-100v200h-100v-400l100 -100v-250z" />
+<glyph unicode="&#xe185;" d="M100 0h300v400q0 41 -29.5 70.5t-70.5 29.5h-100q-41 0 -70.5 -29.5t-29.5 -70.5v-400zM500 0v1000q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-1000h-300zM900 0v700q0 41 29.5 70.5t70.5 29.5h100q41 0 70.5 -29.5t29.5 -70.5v-700h-300z" />
+<glyph unicode="&#xe186;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe187;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h100v200h100v-200h100v500h-100v-200h-100v200h-100v-500zM600 300h200v100h100v300h-100v100h-200v-500 zM700 400v300h100v-300h-100z" />
+<glyph unicode="&#xe188;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v100h-200v300h200v100h-300v-500zM600 300h300v100h-200v300h200v100h-300v-500z" />
+<glyph unicode="&#xe189;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 550l300 -150v300zM600 400l300 150l-300 150v-300z" />
+<glyph unicode="&#xe190;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300v500h700v-500h-700zM300 400h130q41 0 68 42t27 107t-28.5 108t-66.5 43h-130v-300zM575 549 q0 -65 27 -107t68 -42h130v300h-130q-38 0 -66.5 -43t-28.5 -108z" />
+<glyph unicode="&#xe191;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v300h-200v100h200v100h-300v-300h200v-100h-200v-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe192;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 300h300v400h-200v100h-100v-500zM301 400v200h100v-200h-100zM601 300h100v100h-100v-100zM700 700h100 v-400h100v500h-200v-100z" />
+<glyph unicode="&#xe193;" d="M-100 300v500q0 124 88 212t212 88h700q124 0 212 -88t88 -212v-500q0 -124 -88 -212t-212 -88h-700q-124 0 -212 88t-88 212zM100 200h900v700h-900v-700zM200 700v100h300v-300h-99v-100h-100v100h99v200h-200zM201 300v100h100v-100h-100zM601 300v100h100v-100h-100z M700 700v100h200v-500h-100v400h-100z" />
+<glyph unicode="&#xe194;" d="M4 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM186 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 500v200 l100 100h300v-100h-300v-200h300v-100h-300z" />
+<glyph unicode="&#xe195;" d="M0 600q0 162 80 299t217 217t299 80t299 -80t217 -217t80 -299t-80 -299t-217 -217t-299 -80t-299 80t-217 217t-80 299zM182 600q0 -171 121.5 -292.5t292.5 -121.5t292.5 121.5t121.5 292.5t-121.5 292.5t-292.5 121.5t-292.5 -121.5t-121.5 -292.5zM400 400v400h300 l100 -100v-100h-100v100h-200v-100h200v-100h-200v-100h-100zM700 400v100h100v-100h-100z" />
+<glyph unicode="&#xe197;" d="M-14 494q0 -80 56.5 -137t135.5 -57h222v300h400v-300h128q120 0 205 86.5t85 207.5t-85 207t-205 86q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200h200v300h200v-300h200 l-300 -300z" />
+<glyph unicode="&#xe198;" d="M-14 494q0 -80 56.5 -137t135.5 -57h8l414 414l403 -403q94 26 154.5 104.5t60.5 178.5q0 120 -85 206.5t-205 86.5q-46 0 -90 -14q-44 97 -134.5 156.5t-200.5 59.5q-152 0 -260 -107.5t-108 -260.5q0 -25 2 -37q-66 -14 -108.5 -67.5t-42.5 -122.5zM300 200l300 300 l300 -300h-200v-300h-200v300h-200z" />
+<glyph unicode="&#xe199;" d="M100 200h400v-155l-75 -45h350l-75 45v155h400l-270 300h170l-270 300h170l-300 333l-300 -333h170l-270 -300h170z" />
+<glyph unicode="&#xe200;" d="M121 700q0 -53 28.5 -97t75.5 -65q-4 -16 -4 -38q0 -74 52.5 -126.5t126.5 -52.5q56 0 100 30v-306l-75 -45h350l-75 45v306q46 -30 100 -30q74 0 126.5 52.5t52.5 126.5q0 24 -9 55q50 32 79.5 83t29.5 112q0 90 -61.5 155.5t-150.5 71.5q-26 89 -99.5 145.5 t-167.5 56.5q-116 0 -197.5 -81.5t-81.5 -197.5q0 -4 1 -11.5t1 -11.5q-14 2 -23 2q-74 0 -126.5 -52.5t-52.5 -126.5z" />
+</font>
+</defs></svg> \ No newline at end of file
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf
new file mode 100644
index 00000000..67fa00bf
--- /dev/null
+++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.ttf
Binary files differ
diff --git a/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff
new file mode 100644
index 00000000..8c54182a
--- /dev/null
+++ b/rest_framework/static/rest_framework/fonts/glyphicons-halflings-regular.woff
Binary files differ
diff --git a/rest_framework/static/rest_framework/js/bootstrap.min.js b/rest_framework/static/rest_framework/js/bootstrap.min.js
index e0b220f4..7c1561a8 100644
--- a/rest_framework/static/rest_framework/js/bootstrap.min.js
+++ b/rest_framework/static/rest_framework/js/bootstrap.min.js
@@ -1,7 +1,6 @@
-/**
-* Bootstrap.js by @fat & @mdo
-* plugins: bootstrap-transition.js, bootstrap-modal.js, bootstrap-dropdown.js, bootstrap-scrollspy.js, bootstrap-tab.js, bootstrap-tooltip.js, bootstrap-popover.js, bootstrap-affix.js, bootstrap-alert.js, bootstrap-button.js, bootstrap-collapse.js, bootstrap-carousel.js, bootstrap-typeahead.js
-* Copyright 2012 Twitter, Inc.
-* http://www.apache.org/licenses/LICENSE-2.0.txt
-*/
-!function(a){a(function(){a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){var b=function(b,c){this.options=c,this.$element=a(b).delegate('[data-dismiss="modal"]',"click.dismiss.modal",a.proxy(this.hide,this)),this.options.remote&&this.$element.find(".modal-body").load(this.options.remote)};b.prototype={constructor:b,toggle:function(){return this[this.isShown?"hide":"show"]()},show:function(){var b=this,c=a.Event("show");this.$element.trigger(c);if(this.isShown||c.isDefaultPrevented())return;a("body").addClass("modal-open"),this.isShown=!0,this.escape(),this.backdrop(function(){var c=a.support.transition&&b.$element.hasClass("fade");b.$element.parent().length||b.$element.appendTo(document.body),b.$element.show(),c&&b.$element[0].offsetWidth,b.$element.addClass("in").attr("aria-hidden",!1).focus(),b.enforceFocus(),c?b.$element.one(a.support.transition.end,function(){b.$element.trigger("shown")}):b.$element.trigger("shown")})},hide:function(b){b&&b.preventDefault();var c=this;b=a.Event("hide"),this.$element.trigger(b);if(!this.isShown||b.isDefaultPrevented())return;this.isShown=!1,a("body").removeClass("modal-open"),this.escape(),a(document).off("focusin.modal"),this.$element.removeClass("in").attr("aria-hidden",!0),a.support.transition&&this.$element.hasClass("fade")?this.hideWithTransition():this.hideModal()},enforceFocus:function(){var b=this;a(document).on("focusin.modal",function(a){b.$element[0]!==a.target&&!b.$element.has(a.target).length&&b.$element.focus()})},escape:function(){var a=this;this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.modal",function(b){b.which==27&&a.hide()}):this.isShown||this.$element.off("keyup.dismiss.modal")},hideWithTransition:function(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),b.hideModal()},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),b.hideModal()})},hideModal:function(a){this.$element.hide().trigger("hidden"),this.backdrop()},removeBackdrop:function(){this.$backdrop.remove(),this.$backdrop=null},backdrop:function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('<div class="modal-backdrop '+d+'" />').appendTo(document.body),this.options.backdrop!="static"&&this.$backdrop.click(a.proxy(this.hide,this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),e?this.$backdrop.one(a.support.transition.end,b):b()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(a.support.transition.end,a.proxy(this.removeBackdrop,this)):this.removeBackdrop()):b&&b()}},a.fn.modal=function(c){return this.each(function(){var d=a(this),e=d.data("modal"),f=a.extend({},a.fn.modal.defaults,d.data(),typeof c=="object"&&c);e||d.data("modal",e=new b(this,f)),typeof c=="string"?e[c]():f.show&&e.show()})},a.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},a.fn.modal.Constructor=b,a(function(){a("body").on("click.modal.data-api",'[data-toggle="modal"]',function(b){var c=a(this),d=c.attr("href"),e=a(c.attr("data-target")||d&&d.replace(/.*(?=#[^\s]+$)/,"")),f=e.data("modal")?"toggle":a.extend({remote:!/#/.test(d)&&d},e.data(),c.data());b.preventDefault(),e.modal(f).one("hide",function(){c.focus()})})})}(window.jQuery),!function(a){function d(){e(a(b)).removeClass("open")}function e(b){var c=b.attr("data-target"),d;return c||(c=b.attr("href"),c=c&&/#/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,"")),d=a(c),d.length||(d=b.parent()),d}var b="[data-toggle=dropdown]",c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),f,g;if(c.is(".disabled, :disabled"))return;return f=e(c),g=f.hasClass("open"),d(),g||(f.toggleClass("open"),c.focus()),!1},keydown:function(b){var c,d,f,g,h,i;if(!/(38|40|27)/.test(b.keyCode))return;c=a(this),b.preventDefault(),b.stopPropagation();if(c.is(".disabled, :disabled"))return;g=e(c),h=g.hasClass("open");if(!h||h&&b.keyCode==27)return c.click();d=a("[role=menu] li:not(.divider) a",g);if(!d.length)return;i=d.index(d.filter(":focus")),b.keyCode==38&&i>0&&i--,b.keyCode==40&&i<d.length-1&&i++,~i||(i=0),d.eq(i).focus()}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api touchstart.dropdown.data-api",d),a("body").on("click.dropdown touchstart.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api touchstart.dropdown.data-api",b,c.prototype.toggle).on("keydown.dropdown.data-api touchstart.dropdown.data-api",b+", [role=menu]",c.prototype.keydown)})}(window.jQuery),!function(a){function b(b,c){var d=a.proxy(this.process,this),e=a(b).is("body")?a(window):a(b),f;this.options=a.extend({},a.fn.scrollspy.defaults,c),this.$scrollElement=e.on("scroll.scroll-spy.data-api",d),this.selector=(this.options.target||(f=a(b).attr("href"))&&f.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=a("body"),this.refresh(),this.process()}b.prototype={constructor:b,refresh:function(){var b=this,c;this.offsets=a([]),this.targets=a([]),c=this.$body.find(this.selector).map(function(){var b=a(this),c=b.data("target")||b.attr("href"),d=/^#\w/.test(c)&&a(c);return d&&d.length&&[[d.position().top,c]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},process:function(){var a=this.$scrollElement.scrollTop()+this.options.offset,b=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,c=b-this.$scrollElement.height(),d=this.offsets,e=this.targets,f=this.activeTarget,g;if(a>=c)return f!=(g=e.last()[0])&&this.activate(g);for(g=d.length;g--;)f!=e[g]&&a>=d[g]&&(!d[g+1]||a<=d[g+1])&&this.activate(e[g])},activate:function(b){var c,d;this.activeTarget=b,a(this.selector).parent(".active").removeClass("active"),d=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',c=a(d).parent("li").addClass("active"),c.parent(".dropdown-menu").length&&(c=c.closest("li.dropdown").addClass("active")),c.trigger("activate")}},a.fn.scrollspy=function(c){return this.each(function(){var d=a(this),e=d.data("scrollspy"),f=typeof c=="object"&&c;e||d.data("scrollspy",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.scrollspy.Constructor=b,a.fn.scrollspy.defaults={offset:10},a(window).on("load",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);b.scrollspy(b.data())})})}(window.jQuery),!function(a){var b=function(b){this.element=a(b)};b.prototype={constructor:b,show:function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.attr("data-target"),e,f,g;d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,""));if(b.parent("li").hasClass("active"))return;e=c.find(".active a").last()[0],g=a.Event("show",{relatedTarget:e}),b.trigger(g);if(g.isDefaultPrevented())return;f=a(d),this.activate(b.parent("li"),c),this.activate(f,f.parent(),function(){b.trigger({type:"shown",relatedTarget:e})})},activate:function(b,c,d){function g(){e.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),f?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var e=c.find("> .active"),f=d&&a.support.transition&&e.hasClass("fade");f?e.one(a.support.transition.end,g):g(),e.removeClass("in")}},a.fn.tab=function(c){return this.each(function(){var d=a(this),e=d.data("tab");e||d.data("tab",e=new b(this)),typeof c=="string"&&e[c]()})},a.fn.tab.Constructor=b,a(function(){a("body").on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(b){b.preventDefault(),a(this).tab("show")})})}(window.jQuery),!function(a){var b=function(a,b){this.init("tooltip",a,b)};b.prototype={constructor:b,init:function(b,c,d){var e,f;this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.enabled=!0,this.options.trigger=="click"?this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this)):this.options.trigger!="manual"&&(e=this.options.trigger=="hover"?"mouseenter":"focus",f=this.options.trigger=="hover"?"mouseleave":"blur",this.$element.on(e+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(f+"."+this.type,this.options.selector,a.proxy(this.leave,this))),this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(b){return b=a.extend({},a.fn[this.type].defaults,b,this.$element.data()),b.delay&&typeof b.delay=="number"&&(b.delay={show:b.delay,hide:b.delay}),b},enter:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);if(!c.options.delay||!c.options.delay.show)return c.show();clearTimeout(this.timeout),c.hoverState="in",this.timeout=setTimeout(function(){c.hoverState=="in"&&c.show()},c.options.delay.show)},leave:function(b){var c=a(b.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!c.options.delay||!c.options.delay.hide)return c.hide();c.hoverState="out",this.timeout=setTimeout(function(){c.hoverState=="out"&&c.hide()},c.options.delay.hide)},show:function(){var a,b,c,d,e,f,g;if(this.hasContent()&&this.enabled){a=this.tip(),this.setContent(),this.options.animation&&a.addClass("fade"),f=typeof this.options.placement=="function"?this.options.placement.call(this,a[0],this.$element[0]):this.options.placement,b=/in/.test(f),a.remove().css({top:0,left:0,display:"block"}).appendTo(b?this.$element:document.body),c=this.getPosition(b),d=a[0].offsetWidth,e=a[0].offsetHeight;switch(b?f.split(" ")[1]:f){case"bottom":g={top:c.top+c.height,left:c.left+c.width/2-d/2};break;case"top":g={top:c.top-e,left:c.left+c.width/2-d/2};break;case"left":g={top:c.top+c.height/2-e/2,left:c.left-d};break;case"right":g={top:c.top+c.height/2-e/2,left:c.left+c.width}}a.css(g).addClass(f).addClass("in")}},setContent:function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},hide:function(){function d(){var b=setTimeout(function(){c.off(a.support.transition.end).remove()},500);c.one(a.support.transition.end,function(){clearTimeout(b),c.remove()})}var b=this,c=this.tip();return c.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d():c.remove(),this},fixTitle:function(){var a=this.$element;(a.attr("title")||typeof a.attr("data-original-title")!="string")&&a.attr("data-original-title",a.attr("title")||"").removeAttr("title")},hasContent:function(){return this.getTitle()},getPosition:function(b){return a.extend({},b?{top:0,left:0}:this.$element.offset(),{width:this.$element[0].offsetWidth,height:this.$element[0].offsetHeight})},getTitle:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||(typeof c.title=="function"?c.title.call(b[0]):c.title),a},tip:function(){return this.$tip=this.$tip||a(this.options.template)},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(){this[this.tip().hasClass("in")?"hide":"show"]()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}},a.fn.tooltip=function(c){return this.each(function(){var d=a(this),e=d.data("tooltip"),f=typeof c=="object"&&c;e||d.data("tooltip",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.tooltip.Constructor=b,a.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover",title:"",delay:0,html:!0}}(window.jQuery),!function(a){var b=function(a,b){this.init("popover",a,b)};b.prototype=a.extend({},a.fn.tooltip.Constructor.prototype,{constructor:b,setContent:function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content > *")[this.options.html?"html":"text"](c),a.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var a,b=this.$element,c=this.options;return a=b.attr("data-content")||(typeof c.content=="function"?c.content.call(b[0]):c.content),a},tip:function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}}),a.fn.popover=function(c){return this.each(function(){var d=a(this),e=d.data("popover"),f=typeof c=="object"&&c;e||d.data("popover",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.popover.Constructor=b,a.fn.popover.defaults=a.extend({},a.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'<div class="popover"><div class="arrow"></div><div class="popover-inner"><h3 class="popover-title"></h3><div class="popover-content"><p></p></div></div></div>'})}(window.jQuery),!function(a){var b=function(b,c){this.options=a.extend({},a.fn.affix.defaults,c),this.$window=a(window).on("scroll.affix.data-api",a.proxy(this.checkPosition,this)),this.$element=a(b),this.checkPosition()};b.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var b=a(document).height(),c=this.$window.scrollTop(),d=this.$element.offset(),e=this.options.offset,f=e.bottom,g=e.top,h="affix affix-top affix-bottom",i;typeof e!="object"&&(f=g=e),typeof g=="function"&&(g=e.top()),typeof f=="function"&&(f=e.bottom()),i=this.unpin!=null&&c+this.unpin<=d.top?!1:f!=null&&d.top+this.$element.height()>=b-f?"bottom":g!=null&&c<=g?"top":!1;if(this.affixed===i)return;this.affixed=i,this.unpin=i=="bottom"?d.top-c:null,this.$element.removeClass(h).addClass("affix"+(i?"-"+i:""))},a.fn.affix=function(c){return this.each(function(){var d=a(this),e=d.data("affix"),f=typeof c=="object"&&c;e||d.data("affix",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.affix.Constructor=b,a.fn.affix.defaults={offset:0},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var b=a(this),c=b.data();c.offset=c.offset||{},c.offsetBottom&&(c.offset.bottom=c.offsetBottom),c.offsetTop&&(c.offset.top=c.offsetTop),b.affix(c)})})}(window.jQuery),!function(a){var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.closest('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),a.support.transition&&this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();c[a(e).hasClass("in")?"addClass":"removeClass"]("collapsed"),a(e).collapse(f)})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".item.active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition.end&&(this.$element.trigger(a.support.transition.end),this.cycle()),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide",{relatedTarget:e[0]});this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c),g=typeof c=="string"?c:f.slide;e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):g?e[g]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.typeahead.defaults,c),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.$menu=a(this.options.menu).appendTo("body"),this.source=this.options.source,this.shown=!1,this.listen()};b.prototype={constructor:b,select:function(){var a=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(a)).change(),this.hide()},updater:function(a){return a},show:function(){var b=a.extend({},this.$element.offset(),{height:this.$element[0].offsetHeight});return this.$menu.css({top:b.top+b.height,left:b.left}),this.$menu.show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(b){var c;return this.query=this.$element.val(),!this.query||this.query.length<this.options.minLength?this.shown?this.hide():this:(c=a.isFunction(this.source)?this.source(this.query,a.proxy(this.process,this)):this.source,c?this.process(c):this)},process:function(b){var c=this;return b=a.grep(b,function(a){return c.matcher(a)}),b=this.sorter(b),b.length?this.render(b.slice(0,this.options.items)).show():this.shown?this.hide():this},matcher:function(a){return~a.toLowerCase().indexOf(this.query.toLowerCase())},sorter:function(a){var b=[],c=[],d=[],e;while(e=a.shift())e.toLowerCase().indexOf(this.query.toLowerCase())?~e.indexOf(this.query)?c.push(e):d.push(e):b.push(e);return b.concat(c,d)},highlighter:function(a){var b=this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&");return a.replace(new RegExp("("+b+")","ig"),function(a,b){return"<strong>"+b+"</strong>"})},render:function(b){var c=this;return b=a(b).map(function(b,d){return b=a(c.options.item).attr("data-value",d),b.find("a").html(c.highlighter(d)),b[0]}),b.first().addClass("active"),this.$menu.html(b),this},next:function(b){var c=this.$menu.find(".active").removeClass("active"),d=c.next();d.length||(d=a(this.$menu.find("li")[0])),d.addClass("active")},prev:function(a){var b=this.$menu.find(".active").removeClass("active"),c=b.prev();c.length||(c=this.$menu.find("li").last()),c.addClass("active")},listen:function(){this.$element.on("blur",a.proxy(this.blur,this)).on("keypress",a.proxy(this.keypress,this)).on("keyup",a.proxy(this.keyup,this)),(a.browser.chrome||a.browser.webkit||a.browser.msie)&&this.$element.on("keydown",a.proxy(this.keydown,this)),this.$menu.on("click",a.proxy(this.click,this)).on("mouseenter","li",a.proxy(this.mouseenter,this))},move:function(a){if(!this.shown)return;switch(a.keyCode){case 9:case 13:case 27:a.preventDefault();break;case 38:a.preventDefault(),this.prev();break;case 40:a.preventDefault(),this.next()}a.stopPropagation()},keydown:function(b){this.suppressKeyPressRepeat=!~a.inArray(b.keyCode,[40,38,9,13,27]),this.move(b)},keypress:function(a){if(this.suppressKeyPressRepeat)return;this.move(a)},keyup:function(a){switch(a.keyCode){case 40:case 38:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}a.stopPropagation(),a.preventDefault()},blur:function(a){var b=this;setTimeout(function(){b.hide()},150)},click:function(a){a.stopPropagation(),a.preventDefault(),this.select()},mouseenter:function(b){this.$menu.find(".active").removeClass("active"),a(b.currentTarget).addClass("active")}},a.fn.typeahead=function(c){return this.each(function(){var d=a(this),e=d.data("typeahead"),f=typeof c=="object"&&c;e||d.data("typeahead",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.typeahead.defaults={source:[],items:8,menu:'<ul class="typeahead dropdown-menu"></ul>',item:'<li><a href="#"></a></li>',minLength:1},a.fn.typeahead.Constructor=b,a(function(){a("body").on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(b){var c=a(this);if(c.data("typeahead"))return;b.preventDefault(),c.typeahead(c.data())})})}(window.jQuery) \ No newline at end of file
+/*!
+ * Bootstrap v3.2.0 (http://getbootstrap.com)
+ * Copyright 2011-2014 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ */
+if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('<div class="dropdown-backdrop"/>').insertAfter(a(this)).on("click",b);var h={relatedTarget:this};if(f.trigger(d=a.Event("show.bs.dropdown",h)),d.isDefaultPrevented())return;e.trigger("focus"),f.toggleClass("open").trigger("shown.bs.dropdown",h)}return!1}},g.prototype.keydown=function(b){if(/(38|40|27)/.test(b.keyCode)){var d=a(this);if(b.preventDefault(),b.stopPropagation(),!d.is(".disabled, :disabled")){var e=c(d),g=e.hasClass("open");if(!g||g&&27==b.keyCode)return 27==b.which&&e.find(f).trigger("focus"),d.trigger("click");var h=" li:not(.divider):visible a",i=e.find('[role="menu"]'+h+', [role="listbox"]'+h);if(i.length){var j=i.index(i.filter(":focus"));38==b.keyCode&&j>0&&j--,40==b.keyCode&&j<i.length-1&&j++,~j||(j=0),i.eq(j).trigger("focus")}}}};var h=a.fn.dropdown;a.fn.dropdown=d,a.fn.dropdown.Constructor=g,a.fn.dropdown.noConflict=function(){return a.fn.dropdown=h,this},a(document).on("click.bs.dropdown.data-api",b).on("click.bs.dropdown.data-api",".dropdown form",function(a){a.stopPropagation()}).on("click.bs.dropdown.data-api",f,g.prototype.toggle).on("keydown.bs.dropdown.data-api",f+', [role="menu"], [role="listbox"]',g.prototype.keydown)}(jQuery),+function(a){"use strict";function b(b,d){return this.each(function(){var e=a(this),f=e.data("bs.modal"),g=a.extend({},c.DEFAULTS,e.data(),"object"==typeof b&&b);f||e.data("bs.modal",f=new c(this,g)),"string"==typeof b?f[b](d):g.show&&f.show(d)})}var c=function(b,c){this.options=c,this.$body=a(document.body),this.$element=a(b),this.$backdrop=this.isShown=null,this.scrollbarWidth=0,this.options.remote&&this.$element.find(".modal-content").load(this.options.remote,a.proxy(function(){this.$element.trigger("loaded.bs.modal")},this))};c.VERSION="3.2.0",c.DEFAULTS={backdrop:!0,keyboard:!0,show:!0},c.prototype.toggle=function(a){return this.isShown?this.hide():this.show(a)},c.prototype.show=function(b){var c=this,d=a.Event("show.bs.modal",{relatedTarget:b});this.$element.trigger(d),this.isShown||d.isDefaultPrevented()||(this.isShown=!0,this.checkScrollbar(),this.$body.addClass("modal-open"),this.setScrollbar(),this.escape(),this.$element.on("click.dismiss.bs.modal",'[data-dismiss="modal"]',a.proxy(this.hide,this)),this.backdrop(function(){var d=a.support.transition&&c.$element.hasClass("fade");c.$element.parent().length||c.$element.appendTo(c.$body),c.$element.show().scrollTop(0),d&&c.$element[0].offsetWidth,c.$element.addClass("in").attr("aria-hidden",!1),c.enforceFocus();var e=a.Event("shown.bs.modal",{relatedTarget:b});d?c.$element.find(".modal-dialog").one("bsTransitionEnd",function(){c.$element.trigger("focus").trigger(e)}).emulateTransitionEnd(300):c.$element.trigger("focus").trigger(e)}))},c.prototype.hide=function(b){b&&b.preventDefault(),b=a.Event("hide.bs.modal"),this.$element.trigger(b),this.isShown&&!b.isDefaultPrevented()&&(this.isShown=!1,this.$body.removeClass("modal-open"),this.resetScrollbar(),this.escape(),a(document).off("focusin.bs.modal"),this.$element.removeClass("in").attr("aria-hidden",!0).off("click.dismiss.bs.modal"),a.support.transition&&this.$element.hasClass("fade")?this.$element.one("bsTransitionEnd",a.proxy(this.hideModal,this)).emulateTransitionEnd(300):this.hideModal())},c.prototype.enforceFocus=function(){a(document).off("focusin.bs.modal").on("focusin.bs.modal",a.proxy(function(a){this.$element[0]===a.target||this.$element.has(a.target).length||this.$element.trigger("focus")},this))},c.prototype.escape=function(){this.isShown&&this.options.keyboard?this.$element.on("keyup.dismiss.bs.modal",a.proxy(function(a){27==a.which&&this.hide()},this)):this.isShown||this.$element.off("keyup.dismiss.bs.modal")},c.prototype.hideModal=function(){var a=this;this.$element.hide(),this.backdrop(function(){a.$element.trigger("hidden.bs.modal")})},c.prototype.removeBackdrop=function(){this.$backdrop&&this.$backdrop.remove(),this.$backdrop=null},c.prototype.backdrop=function(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;if(this.$backdrop=a('<div class="modal-backdrop '+d+'" />').appendTo(this.$body),this.$element.on("click.dismiss.bs.modal",a.proxy(function(a){a.target===a.currentTarget&&("static"==this.options.backdrop?this.$element[0].focus.call(this.$element[0]):this.hide.call(this))},this)),e&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in"),!b)return;e?this.$backdrop.one("bsTransitionEnd",b).emulateTransitionEnd(150):b()}else if(!this.isShown&&this.$backdrop){this.$backdrop.removeClass("in");var f=function(){c.removeBackdrop(),b&&b()};a.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one("bsTransitionEnd",f).emulateTransitionEnd(150):f()}else b&&b()},c.prototype.checkScrollbar=function(){document.body.clientWidth>=window.innerWidth||(this.scrollbarWidth=this.scrollbarWidth||this.measureScrollbar())},c.prototype.setScrollbar=function(){var a=parseInt(this.$body.css("padding-right")||0,10);this.scrollbarWidth&&this.$body.css("padding-right",a+this.scrollbarWidth)},c.prototype.resetScrollbar=function(){this.$body.css("padding-right","")},c.prototype.measureScrollbar=function(){var a=document.createElement("div");a.className="modal-scrollbar-measure",this.$body.append(a);var b=a.offsetWidth-a.clientWidth;return this.$body[0].removeChild(a),b};var d=a.fn.modal;a.fn.modal=b,a.fn.modal.Constructor=c,a.fn.modal.noConflict=function(){return a.fn.modal=d,this},a(document).on("click.bs.modal.data-api",'[data-toggle="modal"]',function(c){var d=a(this),e=d.attr("href"),f=a(d.attr("data-target")||e&&e.replace(/.*(?=#[^\s]+$)/,"")),g=f.data("bs.modal")?"toggle":a.extend({remote:!/#/.test(e)&&e},f.data(),d.data());d.is("a")&&c.preventDefault(),f.one("show.bs.modal",function(a){a.isDefaultPrevented()||f.one("hidden.bs.modal",function(){d.is(":visible")&&d.trigger("focus")})}),b.call(f,g,this)})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tooltip"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.tooltip",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.type=this.options=this.enabled=this.timeout=this.hoverState=this.$element=null,this.init("tooltip",a,b)};c.VERSION="3.2.0",c.DEFAULTS={animation:!0,placement:"top",selector:!1,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(this.options.viewport.selector||this.options.viewport);for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show()},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var c=a.contains(document.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!c)return;var d=this,e=this.tip(),f=this.getUID(this.type);this.setContent(),e.attr("id",f),this.$element.attr("aria-describedby",f),this.options.animation&&e.addClass("fade");var g="function"==typeof this.options.placement?this.options.placement.call(this,e[0],this.$element[0]):this.options.placement,h=/\s?auto?\s?/i,i=h.test(g);i&&(g=g.replace(h,"")||"top"),e.detach().css({top:0,left:0,display:"block"}).addClass(g).data("bs."+this.type,this),this.options.container?e.appendTo(this.options.container):e.insertAfter(this.$element);var j=this.getPosition(),k=e[0].offsetWidth,l=e[0].offsetHeight;if(i){var m=g,n=this.$element.parent(),o=this.getPosition(n);g="bottom"==g&&j.top+j.height+l-o.scroll>o.height?"top":"top"==g&&j.top-o.scroll-l<0?"bottom":"right"==g&&j.right+k>o.width?"left":"left"==g&&j.left-k<o.left?"right":g,e.removeClass(m).addClass(g)}var p=this.getCalculatedOffset(g,j,k,l);this.applyPlacement(p,g);var q=function(){d.$element.trigger("shown.bs."+d.type),d.hoverState=null};a.support.transition&&this.$tip.hasClass("fade")?e.one("bsTransitionEnd",q).emulateTransitionEnd(150):q()}},c.prototype.applyPlacement=function(b,c){var d=this.tip(),e=d[0].offsetWidth,f=d[0].offsetHeight,g=parseInt(d.css("margin-top"),10),h=parseInt(d.css("margin-left"),10);isNaN(g)&&(g=0),isNaN(h)&&(h=0),b.top=b.top+g,b.left=b.left+h,a.offset.setOffset(d[0],a.extend({using:function(a){d.css({top:Math.round(a.top),left:Math.round(a.left)})}},b),0),d.addClass("in");var i=d[0].offsetWidth,j=d[0].offsetHeight;"top"==c&&j!=f&&(b.top=b.top+f-j);var k=this.getViewportAdjustedDelta(c,b,i,j);k.left?b.left+=k.left:b.top+=k.top;var l=k.left?2*k.left-e+i:2*k.top-f+j,m=k.left?"left":"top",n=k.left?"offsetWidth":"offsetHeight";d.offset(b),this.replaceArrow(l,d[0][n],m)},c.prototype.replaceArrow=function(a,b,c){this.arrow().css(c,a?50*(1-a/b)+"%":"")},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle();a.find(".tooltip-inner")[this.options.html?"html":"text"](b),a.removeClass("fade in top bottom left right")},c.prototype.hide=function(){function b(){"in"!=c.hoverState&&d.detach(),c.$element.trigger("hidden.bs."+c.type)}var c=this,d=this.tip(),e=a.Event("hide.bs."+this.type);return this.$element.removeAttr("aria-describedby"),this.$element.trigger(e),e.isDefaultPrevented()?void 0:(d.removeClass("in"),a.support.transition&&this.$tip.hasClass("fade")?d.one("bsTransitionEnd",b).emulateTransitionEnd(150):b(),this.hoverState=null,this)},c.prototype.fixTitle=function(){var a=this.$element;(a.attr("title")||"string"!=typeof a.attr("data-original-title"))&&a.attr("data-original-title",a.attr("title")||"").attr("title","")},c.prototype.hasContent=function(){return this.getTitle()},c.prototype.getPosition=function(b){b=b||this.$element;var c=b[0],d="BODY"==c.tagName;return a.extend({},"function"==typeof c.getBoundingClientRect?c.getBoundingClientRect():null,{scroll:d?document.documentElement.scrollTop||document.body.scrollTop:b.scrollTop(),width:d?a(window).width():b.outerWidth(),height:d?a(window).height():b.outerHeight()},d?{top:0,left:0}:b.offset())},c.prototype.getCalculatedOffset=function(a,b,c,d){return"bottom"==a?{top:b.top+b.height,left:b.left+b.width/2-c/2}:"top"==a?{top:b.top-d,left:b.left+b.width/2-c/2}:"left"==a?{top:b.top+b.height/2-d/2,left:b.left-c}:{top:b.top+b.height/2-d/2,left:b.left+b.width}},c.prototype.getViewportAdjustedDelta=function(a,b,c,d){var e={top:0,left:0};if(!this.$viewport)return e;var f=this.options.viewport&&this.options.viewport.padding||0,g=this.getPosition(this.$viewport);if(/right|left/.test(a)){var h=b.top-f-g.scroll,i=b.top+f-g.scroll+d;h<g.top?e.top=g.top-h:i>g.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;j<g.left?e.left=g.left-j:k>g.width&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){return this.$tip=this.$tip||a(this.options.template)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.validate=function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){clearTimeout(this.timeout),this.hide().$element.off("."+this.type).removeData("bs."+this.type)};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;(e||"destroy"!=b)&&(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.2.0",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>'}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").empty()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")},c.prototype.tip=function(){return this.$tip||(this.$tip=a(this.options.template)),this.$tip};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){var e=a.proxy(this.process,this);this.$body=a("body"),this.$scrollElement=a(a(c).is("body")?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",e),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.2.0",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b="offset",c=0;a.isWindow(this.$scrollElement[0])||(b="position",c=this.$scrollElement.scrollTop()),this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight();var d=this;this.$body.find(this.selector).map(function(){var d=a(this),e=d.data("target")||d.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[b]().top+c,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){d.offsets.push(this[0]),d.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b<=e[0])return g!=(a=f[0])&&this.activate(a);for(a=e.length;a--;)g!=f[a]&&b>=e[a]&&(!e[a+1]||b<=e[a+1])&&this.activate(f[a])},b.prototype.activate=function(b){this.activeTarget=b,a(this.selector).parentsUntil(this.options.target,".active").removeClass("active");var c=this.selector+'[data-target="'+b+'"],'+this.selector+'[href="'+b+'"]',d=a(c).parents("li").addClass("active");d.parent(".dropdown-menu").length&&(d=d.closest("li.dropdown").addClass("active")),d.trigger("activate.bs.scrollspy")};var d=a.fn.scrollspy;a.fn.scrollspy=c,a.fn.scrollspy.Constructor=b,a.fn.scrollspy.noConflict=function(){return a.fn.scrollspy=d,this},a(window).on("load.bs.scrollspy.data-api",function(){a('[data-spy="scroll"]').each(function(){var b=a(this);c.call(b,b.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.tab");e||d.data("bs.tab",e=new c(this)),"string"==typeof b&&e[b]()})}var c=function(b){this.element=a(b)};c.VERSION="3.2.0",c.prototype.show=function(){var b=this.element,c=b.closest("ul:not(.dropdown-menu)"),d=b.data("target");if(d||(d=b.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),!b.parent("li").hasClass("active")){var e=c.find(".active:last a")[0],f=a.Event("show.bs.tab",{relatedTarget:e});if(b.trigger(f),!f.isDefaultPrevented()){var g=a(d);this.activate(b.closest("li"),c),this.activate(g,g.parent(),function(){b.trigger({type:"shown.bs.tab",relatedTarget:e})})}}},c.prototype.activate=function(b,c,d){function e(){f.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),b.addClass("active"),g?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu")&&b.closest("li.dropdown").addClass("active"),d&&d()}var f=c.find("> .active"),g=d&&a.support.transition&&f.hasClass("fade");g?f.one("bsTransitionEnd",e).emulateTransitionEnd(150):e(),f.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this},a(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(c){c.preventDefault(),b.call(a(this),"show")})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=this.unpin=this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.2.0",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=a(document).height(),d=this.$target.scrollTop(),e=this.$element.offset(),f=this.options.offset,g=f.top,h=f.bottom;"object"!=typeof f&&(h=g=f),"function"==typeof g&&(g=f.top(this.$element)),"function"==typeof h&&(h=f.bottom(this.$element));var i=null!=this.unpin&&d+this.unpin<=e.top?!1:null!=h&&e.top+this.$element.height()>=b-h?"bottom":null!=g&&g>=d?"top":!1;if(this.affixed!==i){null!=this.unpin&&this.$element.css("top","");var j="affix"+(i?"-"+i:""),k=a.Event(j+".bs.affix");this.$element.trigger(k),k.isDefaultPrevented()||(this.affixed=i,this.unpin="bottom"==i?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(j).trigger(a.Event(j.replace("affix","affixed"))),"bottom"==i&&this.$element.offset({top:b-this.$element.height()-h}))}}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},d.offsetBottom&&(d.offset.bottom=d.offsetBottom),d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file
diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js
index c74829d7..c8812132 100644
--- a/rest_framework/static/rest_framework/js/default.js
+++ b/rest_framework/static/rest_framework/js/default.js
@@ -1,13 +1,57 @@
+function getCookie(c_name)
+{
+ // From http://www.w3schools.com/js/js_cookies.asp
+ var c_value = document.cookie;
+ var c_start = c_value.indexOf(" " + c_name + "=");
+ if (c_start == -1) {
+ c_start = c_value.indexOf(c_name + "=");
+ }
+ if (c_start == -1) {
+ c_value = null;
+ } else {
+ c_start = c_value.indexOf("=", c_start) + 1;
+ var c_end = c_value.indexOf(";", c_start);
+ if (c_end == -1) {
+ c_end = c_value.length;
+ }
+ c_value = unescape(c_value.substring(c_start,c_end));
+ }
+ return c_value;
+}
+
+// JSON highlighting.
prettyPrint();
+// Bootstrap tooltips.
$('.js-tooltip').tooltip({
- delay: 1000
+ delay: 1000,
+ container: 'body'
});
+// Deal with rounded tab styling after tab clicks.
$('a[data-toggle="tab"]:first').on('shown', function (e) {
$(e.target).parents('.tabbable').addClass('first-tab-active');
});
$('a[data-toggle="tab"]:not(:first)').on('shown', function (e) {
$(e.target).parents('.tabbable').removeClass('first-tab-active');
});
-$('.form-switcher a:first').tab('show');
+
+$('a[data-toggle="tab"]').click(function(){
+ document.cookie="tabstyle=" + this.name + "; path=/";
+});
+
+// Store tab preference in cookies & display appropriate tab on load.
+var selectedTab = null;
+var selectedTabName = getCookie('tabstyle');
+
+if (selectedTabName) {
+ selectedTab = $('.form-switcher a[name=' + selectedTabName + ']');
+}
+
+if (selectedTab && selectedTab.length > 0) {
+ // Display whichever tab is selected.
+ selectedTab.tab('show');
+} else {
+ // If no tab selected, display rightmost tab.
+ $('.form-switcher a:first').tab('show');
+}
diff --git a/rest_framework/status.py b/rest_framework/status.py
index b9f249f9..90a75508 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -6,6 +6,27 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585
"""
from __future__ import unicode_literals
+
+def is_informational(code):
+ return code >= 100 and code <= 199
+
+
+def is_success(code):
+ return code >= 200 and code <= 299
+
+
+def is_redirect(code):
+ return code >= 300 and code <= 399
+
+
+def is_client_error(code):
+ return code >= 400 and code <= 499
+
+
+def is_server_error(code):
+ return code >= 500 and code <= 599
+
+
HTTP_100_CONTINUE = 100
HTTP_101_SWITCHING_PROTOCOLS = 101
HTTP_200_OK = 200
diff --git a/rest_framework/templates/rest_framework/api_form.html b/rest_framework/templates/rest_framework/api_form.html
new file mode 100644
index 00000000..96f924ed
--- /dev/null
+++ b/rest_framework/templates/rest_framework/api_form.html
@@ -0,0 +1,8 @@
+{% load rest_framework %}
+{% csrf_token %}
+{% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+{% endfor %}
+<!-- form.non_field_errors -->
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 9d939e73..877387f2 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -1,236 +1,259 @@
{% load url from future %}
+{% load staticfiles %}
{% load rest_framework %}
<!DOCTYPE html>
<html>
<head>
- {% block head %}
+ {% block head %}
- {% block meta %}
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="robots" content="NONE,NOARCHIVE" />
- {% endblock %}
+ {% block meta %}
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="robots" content="NONE,NOARCHIVE" />
+ {% endblock %}
- <title>{% block title %}Django REST framework{% endblock %}</title>
+ <title>{% block title %}Django REST framework{% endblock %}</title>
- {% block style %}
- {% block bootstrap_theme %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
- {% endblock %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
- {% endblock %}
+ {% block style %}
+ {% block bootstrap_theme %}
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
+ {% endblock %}
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
+ {% endblock %}
- {% endblock %}
+ {% endblock %}
</head>
- <body class="{% block bodyclass %}{% endblock %} container">
-
- <div class="wrapper">
-
- {% block navbar %}
- <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
- <div class="navbar-inner">
- <div class="container-fluid">
- <span href="/">
- {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
- </span>
- <ul class="nav pull-right">
- {% block userlinks %}
- {% if user.is_authenticated %}
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- {{ user }}
- <b class="caret"></b>
+ {% block body %}
+ <body class="{% block bodyclass %}{% endblock %}">
+
+ <div class="wrapper">
+
+ {% block navbar %}
+ <div class="navbar navbar-static-top {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
+ <div class="container">
+ <span>
+ {% block branding %}
+ <a class='navbar-brand' rel="nofollow" href='http://www.django-rest-framework.org'>
+ Django REST framework <span class="version">{{ version }}</span>
</a>
- <ul class="dropdown-menu">
- <li>{% optional_logout request %}</li>
- </ul>
- </li>
+ {% endblock %}
+ </span>
+ <ul class="nav navbar-nav pull-right">
+ {% block userlinks %}
+ {% if user.is_authenticated %}
+ {% optional_logout request user %}
+ {% else %}
+ {% optional_login request %}
+ {% endif %}
+ {% endblock %}
+ </ul>
+ </div>
+ </div>
+ {% endblock %}
+
+ <div class="container">
+ {% block breadcrumbs %}
+ <ul class="breadcrumb">
+ {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
+ {% if forloop.last %}
+ <li class="active"><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% else %}
- <li>{% optional_login request %}</li>
+ <li><a href="{{ breadcrumb_url }}">{{ breadcrumb_name }}</a></li>
{% endif %}
- {% endblock %}
+ {% endfor %}
</ul>
- </div>
- </div>
- </div>
- {% endblock %}
+ {% endblock %}
- {% block breadcrumbs %}
- <ul class="breadcrumb">
- {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
- <li>
- <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
- </li>
- {% endfor %}
- </ul>
- {% endblock %}
+ <!-- Content -->
+ <div id="content">
- <!-- Content -->
- <div id="content">
-
- {% if 'GET' in allowed_methods %}
- <form id="get-form" class="pull-right">
- <fieldset>
- <div class="btn-group format-selection">
- <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
-
- <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu">
- {% for format in available_formats %}
- <li>
- <a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
- </li>
- {% endfor %}
- </ul>
- </div>
+ {% if 'GET' in allowed_methods %}
+ <form id="get-form" class="pull-right">
+ <fieldset>
+ <div class="btn-group format-selection">
+ <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}'
+ rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
- </fieldset>
- </form>
- {% endif %}
-
- {% if options_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
- {% csrf_token %}
- <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
- <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
- </form>
- {% endif %}
-
- {% if delete_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
- {% csrf_token %}
- <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
- <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
- </form>
- {% endif %}
-
- <div class="content-main">
- <div class="page-header"><h1>{{ name }}</h1></div>
- {{ description }}
- <div class="request-info" style="clear: both" >
- <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
- </div>
- <div class="response-info">
- <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
-{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
-{% endfor %}
-</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
- </div>
- </div>
+ <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown"
+ title="Specify a format for the GET request">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ {% for format in available_formats %}
+ <li>
+ <a class="js-tooltip format-option"
+ href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}'
+ rel="nofollow"
+ title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">
+ {{ format }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </fieldset>
+ </form>
+ {% endif %}
+
+ {% if options_form %}
+ <form class="button-form" action="{{ request.get_full_path }}" method="POST">
+ {% csrf_token %}
+ <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
+ <button class="btn btn-primary js-tooltip"
+ title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
+ </form>
+ {% endif %}
- {% if response.status_code != 403 %}
+ {% if delete_form %}
+ <form class="button-form" action="{{ request.get_full_path }}" method="POST">
+ {% csrf_token %}
+ <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
+ <button class="btn btn-danger js-tooltip"
+ title="Make a DELETE request on the {{ name }} resource">DELETE</button>
+ </form>
+ {% endif %}
+
+ <div class="content-main">
+ <div class="page-header">
+ <h1>{{ name }}</h1>
+ </div>
+ <div style="float:left">
+ {% block description %}
+ {{ description }}
+ {% endblock %}
+ </div>
- {% if post_form or raw_data_post_form %}
- <div {% if post_form %}class="tabbable"{% endif %}>
- {% if post_form %}
- <ul class="nav nav-tabs form-switcher">
- <li><a href="#object-form" data-toggle="tab">HTML form</a></li>
- <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
- </ul>
+ {% if paginator %}
+ <nav style="float: right">
+ {% get_pagination_html paginator %}
+ </nav>
{% endif %}
- <div class="well tab-content">
- {% if post_form %}
- <div class="tab-pane" id="object-form">
- {% with form=post_form %}
- <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
- <fieldset>
- {% include "rest_framework/form.html" %}
- <div class="form-actions">
- <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
- </div>
- </fieldset>
- </form>
- {% endwith %}
- </div>
- {% endif %}
- <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
- {% with form=raw_data_post_form %}
- <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
- <fieldset>
- {% include "rest_framework/form.html" %}
- <div class="form-actions">
- <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
- </div>
- </fieldset>
- </form>
- {% endwith %}
- </div>
+
+ <div class="request-info" style="clear: both" >
+ <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
+ </div>
+ <div class="response-info">
+ <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
+{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
+{% endfor %}
+</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div>
</div>
- {% endif %}
- {% if put_form or raw_data_put_form or raw_data_patch_form %}
- <div {% if put_form %}class="tabbable"{% endif %}>
- {% if put_form %}
- <ul class="nav nav-tabs form-switcher">
- <li><a href="#object-form" data-toggle="tab">HTML form</a></li>
- <li><a href="#generic-content-form" data-toggle="tab">Raw data</a></li>
- </ul>
- {% endif %}
- <div class="well tab-content">
- {% if put_form %}
- <div class="tab-pane" id="object-form">
- {% with form=put_form %}
- <form action="{{ request.get_full_path }}" method="POST" {% if form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
- <fieldset>
- {% include "rest_framework/form.html" %}
- <div class="form-actions">
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ {% if display_edit_forms %}
+
+ {% if post_form or raw_data_post_form %}
+ <div {% if post_form %}class="tabbable"{% endif %}>
+ {% if post_form %}
+ <ul class="nav nav-tabs form-switcher">
+ <li>
+ <a name='html-tab' href="#post-object-form" data-toggle="tab">HTML form</a>
+ </li>
+ <li>
+ <a name='raw-tab' href="#post-generic-content-form" data-toggle="tab">Raw data</a>
+ </li>
+ </ul>
+ {% endif %}
+ <div class="well tab-content">
+ {% if post_form %}
+ <div class="tab-pane" id="post-object-form">
+ {% with form=post_form %}
+ <form action="{{ request.get_full_path }}"
+ method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
+ <fieldset>
+ {{ post_form }}
+ <div class="form-actions">
+ <button class="btn btn-primary"
+ title="Make a POST request on the {{ name }} resource">POST</button>
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
</div>
- </fieldset>
- </form>
- {% endwith %}
+ {% endif %}
+ <div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
+ {% with form=raw_data_post_form %}
+ <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
+ <fieldset>
+ {% include "rest_framework/raw_data_form.html" %}
+ <div class="form-actions">
+ <button class="btn btn-primary"
+ title="Make a POST request on the {{ name }} resource">POST</button>
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
+ </div>
+ </div>
</div>
- {% endif %}
- <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
- {% with form=raw_data_put_or_patch_form %}
- <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
- <fieldset>
- {% include "rest_framework/form.html" %}
- <div class="form-actions">
- {% if raw_data_put_form %}
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
- {% endif %}
- {% if raw_data_patch_form %}
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PUT request on the {{ name }} resource">PATCH</button>
- {% endif %}
+ {% endif %}
+
+ {% if put_form or raw_data_put_form or raw_data_patch_form %}
+ <div {% if put_form %}class="tabbable"{% endif %}>
+ {% if put_form %}
+ <ul class="nav nav-tabs form-switcher">
+ <li>
+ <a name='html-tab' href="#put-object-form" data-toggle="tab">HTML form</a>
+ </li>
+ <li>
+ <a name='raw-tab' href="#put-generic-content-form" data-toggle="tab">Raw data</a>
+ </li>
+ </ul>
+ {% endif %}
+ <div class="well tab-content">
+ {% if put_form %}
+ <div class="tab-pane" id="put-object-form">
+ <form action="{{ request.get_full_path }}"
+ method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
+ <fieldset>
+ {{ put_form }}
+ <div class="form-actions">
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ </div>
+ </fieldset>
+ </form>
</div>
- </fieldset>
- </form>
- {% endwith %}
+ {% endif %}
+ <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
+ {% with form=raw_data_put_or_patch_form %}
+ <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
+ <fieldset>
+ {% include "rest_framework/raw_data_form.html" %}
+ <div class="form-actions">
+ {% if raw_data_put_form %}
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ {% endif %}
+ {% if raw_data_patch_form %}
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
+ {% endif %}
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
+ </div>
+ </div>
</div>
- </div>
- </div>
+ {% endif %}
{% endif %}
- {% endif %}
-
- </div>
- <!-- END content-main -->
-
- </div>
- <!-- END Content -->
-
- <div id="push"></div>
-
- </div>
-
- </div><!-- ./wrapper -->
-
- {% block footer %}
- <!--<div id="footer">
- <a class="powered-by" href='http://django-rest-framework.org'>Django REST framework</a>
- </div>-->
- {% endblock %}
+ </div>
+ <!-- END Content -->
+ </div><!-- /.container -->
+ </div><!-- ./wrapper -->
- {% block script %}
- <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
- <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
- <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
- <script src="{% static "rest_framework/js/default.js" %}"></script>
+ {% block script %}
+ <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
+ <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/default.js" %}"></script>
+ {% endblock %}
+ </body>
{% endblock %}
- </body>
</html>
diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html
deleted file mode 100644
index b27f652e..00000000
--- a/rest_framework/templates/rest_framework/form.html
+++ /dev/null
@@ -1,13 +0,0 @@
-{% load rest_framework %}
-{% csrf_token %}
-{{ form.non_field_errors }}
-{% for field in form %}
- <div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
- {{ field.label_tag|add_class:"control-label" }}
- <div class="controls">
- {{ field }}
- <span class="help-block">{{ field.help_text }}</span>
- <!--{{ field.errors|add_class:"help-block" }}-->
- </div>
- </div>
-{% endfor %}
diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox.html b/rest_framework/templates/rest_framework/horizontal/checkbox.html
new file mode 100644
index 00000000..07a7308f
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/checkbox.html
@@ -0,0 +1,16 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ <div class="col-sm-offset-2 col-sm-10">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}>
+ {% if field.label %}{{ field.label }}{% endif %}
+ </label>
+ </div>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html
new file mode 100644
index 00000000..ec7d5935
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html
@@ -0,0 +1,30 @@
+<div class="form-group">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ {% if style.inline %}
+ {% for key, text in field.choices.items %}
+ <label class="checkbox-inline">
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/fieldset.html b/rest_framework/templates/rest_framework/horizontal/fieldset.html
new file mode 100644
index 00000000..ba3e3aba
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/fieldset.html
@@ -0,0 +1,13 @@
+{% load rest_framework %}
+<fieldset>
+ {% if field.label %}
+ <div class="form-group" style="border-bottom: 1px solid #e5e5e5">
+ <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
+ </div>
+ {% endif %}
+ {% for nested_field in field %}
+ {% if not nested_field.read_only %}
+ {% render_field nested_field style=style %}
+ {% endif %}
+ {% endfor %}
+</fieldset>
diff --git a/rest_framework/templates/rest_framework/horizontal/form.html b/rest_framework/templates/rest_framework/horizontal/form.html
new file mode 100644
index 00000000..fd15b626
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/form.html
@@ -0,0 +1,15 @@
+{% load rest_framework %}
+<form class="form-horizontal" role="form" action="." method="POST" novalidate>
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+ <!-- form.non_field_errors -->
+ <div class="form-group">
+ <div class="col-sm-offset-2 col-sm-10">
+ <button type="submit" class="btn btn-default">Submit</button>
+ </div>
+ </div>
+</form>
diff --git a/rest_framework/templates/rest_framework/horizontal/input.html b/rest_framework/templates/rest_framework/horizontal/input.html
new file mode 100644
index 00000000..c41cd523
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/input.html
@@ -0,0 +1,14 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
new file mode 100644
index 00000000..a9ff04a6
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
@@ -0,0 +1,16 @@
+{% load rest_framework %}
+<fieldset>
+ {% if field.label %}
+ <div class="form-group" style="border-bottom: 1px solid #e5e5e5">
+ <legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
+ </div>
+ {% endif %}
+ <!--
+ <ul>
+ {% for child in field.value %}
+ <li>TODO</li>
+ {% endfor %}
+ </ul>
+ -->
+ <p>Lists are not currently supported in HTML input.</p>
+</fieldset>
diff --git a/rest_framework/templates/rest_framework/horizontal/radio.html b/rest_framework/templates/rest_framework/horizontal/radio.html
new file mode 100644
index 00000000..52238bb1
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/radio.html
@@ -0,0 +1,30 @@
+<div class="form-group">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ {% if style.inline %}
+ {% for key, text in field.choices.items %}
+ <label class="radio-inline">
+ <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ {% endfor %}
+ {% else %}
+ {% for key, text in field.choices.items %}
+ <div class="radio">
+ <label>
+ <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
new file mode 100644
index 00000000..8a7fca37
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -0,0 +1,21 @@
+<div class="form-group">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ <select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null or field.allow_blank %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
+ {% endfor %}
+ </select>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/select_multiple.html b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
new file mode 100644
index 00000000..0735f280
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/select_multiple.html
@@ -0,0 +1,23 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+<div class="form-group">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
+ {% endfor %}
+ </select>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/horizontal/textarea.html b/rest_framework/templates/rest_framework/horizontal/textarea.html
new file mode 100644
index 00000000..ec107549
--- /dev/null
+++ b/rest_framework/templates/rest_framework/horizontal/textarea.html
@@ -0,0 +1,14 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="col-sm-2 control-label {% if style.hide_label %}sr-only{% endif %}">{{ field.label }}</label>
+ {% endif %}
+ <div class="col-sm-10">
+ <textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/checkbox.html b/rest_framework/templates/rest_framework/inline/checkbox.html
new file mode 100644
index 00000000..71737f15
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/checkbox.html
@@ -0,0 +1,8 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="true" {% if field.value %}checked{% endif %}>
+ {% if field.label %}{{ field.label }}{% endif %}
+ </label>
+ </div>
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
new file mode 100644
index 00000000..09349686
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
@@ -0,0 +1,13 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ {% for key, text in field.choices.items %}
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/fieldset.html b/rest_framework/templates/rest_framework/inline/fieldset.html
new file mode 100644
index 00000000..e49b42fd
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/fieldset.html
@@ -0,0 +1,6 @@
+{% load rest_framework %}
+{% for nested_field in field %}
+ {% if not nested_field.read_only %}
+ {% render_field nested_field style=style %}
+ {% endif %}
+{% endfor %}
diff --git a/rest_framework/templates/rest_framework/inline/form.html b/rest_framework/templates/rest_framework/inline/form.html
new file mode 100644
index 00000000..6a0ea81d
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/form.html
@@ -0,0 +1,11 @@
+{% load rest_framework %}
+<form class="form-inline" role="form" action="." method="POST" novalidate>
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+ <!-- form.non_field_errors -->
+ <button type="submit" class="btn btn-default">Submit</button>
+</form>
diff --git a/rest_framework/templates/rest_framework/inline/input.html b/rest_framework/templates/rest_framework/inline/input.html
new file mode 100644
index 00000000..de85ba48
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/input.html
@@ -0,0 +1,6 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/list_fieldset.html b/rest_framework/templates/rest_framework/inline/list_fieldset.html
new file mode 100644
index 00000000..2ae56d7c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/list_fieldset.html
@@ -0,0 +1 @@
+<span>Lists are not currently supported in HTML input.</span>
diff --git a/rest_framework/templates/rest_framework/inline/radio.html b/rest_framework/templates/rest_framework/inline/radio.html
new file mode 100644
index 00000000..1915f4f8
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/radio.html
@@ -0,0 +1,13 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ {% for key, text in field.choices.items %}
+ <div class="radio">
+ <label>
+ <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html
new file mode 100644
index 00000000..6b30e4d6
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/select.html
@@ -0,0 +1,13 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ <select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null or field.allow_blank %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
+ {% endfor %}
+ </select>
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/select_multiple.html b/rest_framework/templates/rest_framework/inline/select_multiple.html
new file mode 100644
index 00000000..5a8b2494
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/select_multiple.html
@@ -0,0 +1,15 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
+ {% endfor %}
+ </select>
+</div>
diff --git a/rest_framework/templates/rest_framework/inline/textarea.html b/rest_framework/templates/rest_framework/inline/textarea.html
new file mode 100644
index 00000000..0766a01c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/textarea.html
@@ -0,0 +1,6 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label class="sr-only">{{ field.label }}</label>
+ {% endif %}
+ <input name="{{ field.name }}" type="text" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
+</div>
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index be9a0072..8e6240a6 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -1,17 +1,9 @@
+{% extends "rest_framework/base.html" %}
{% load url from future %}
+{% load staticfiles %}
{% load rest_framework %}
-<html>
-
- <head>
- {% block style %}
- {% block bootstrap_theme %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
- {% endblock %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
- {% endblock %}
- </head>
+ {% block body %}
<body class="container">
<div class="container-fluid" style="margin-top: 30px">
@@ -25,23 +17,46 @@
<div class="row-fluid">
<div>
- <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
+ <form action="{% url 'rest_framework:login' %}" role="form" method="post">
{% csrf_token %}
- <div id="div_id_username" class="clearfix control-group">
- <div class="controls">
- <Label class="span4">Username:</label>
- <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
+ <div id="div_id_username"
+ class="clearfix control-group {% if form.username.errors %}error{% endif %}">
+ <div class="form-group">
+ <label for="id_username">Username:</label>
+ <input type="text" name="username" maxlength="100"
+ autocapitalize="off"
+ autocorrect="off" class="form-control textinput textInput"
+ id="id_username" required
+ {% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
+ {% if form.username.errors %}
+ <p class="text-error">
+ {{ form.username.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
- <div id="div_id_password" class="clearfix control-group">
- <div class="controls">
- <Label class="span4">Password:</label>
- <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
+ <div id="div_id_password"
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ <div class="form-group">
+ <label for="id_password">Password:</label>
+ <input type="password" name="password" maxlength="100"
+ autocapitalize="off" autocorrect="off" class="form-control textinput textInput"
+ id="id_password" required>
+ {% if form.password.errors %}
+ <p class="text-error">
+ {{ form.password.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
<input type="hidden" name="next" value="{{ next }}" />
+ {% if form.non_field_errors %}
+ {% for error in form.non_field_errors %}
+ <div class="well well-small text-error" style="border: none">{{ error }}</div>
+ {% endfor %}
+ {% endif %}
<div class="form-actions-no-box">
- <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
+ <input type="submit" name="submit" value="Log in" class="btn btn-primary form-control" id="submit-id-submit">
</div>
</form>
</div>
@@ -50,4 +65,4 @@
</div><!-- /.row-fluid -->
</div><!-- /.container-fluid -->
</body>
-</html>
+ {% endblock %}
diff --git a/rest_framework/templates/rest_framework/pagination/numbers.html b/rest_framework/templates/rest_framework/pagination/numbers.html
new file mode 100644
index 00000000..04045810
--- /dev/null
+++ b/rest_framework/templates/rest_framework/pagination/numbers.html
@@ -0,0 +1,27 @@
+<ul class="pagination" style="margin: 5px 0 10px 0">
+ {% if previous_url %}
+ <li><a href="{{ previous_url }}" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>
+ {% else %}
+ <li class="disabled"><a href="#" aria-label="Previous"><span aria-hidden="true">&laquo;</span></a></li>
+ {% endif %}
+
+ {% for page_link in page_links %}
+ {% if page_link.is_break %}
+ <li class="disabled">
+ <a href="#"><span aria-hidden="true">&hellip;</span></a>
+ </li>
+ {% else %}
+ {% if page_link.is_active %}
+ <li class="active"><a href="{{ page_link.url }}">{{ page_link.number }}</a></li>
+ {% else %}
+ <li><a href="{{ page_link.url }}">{{ page_link.number }}</a></li>
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+
+ {% if next_url %}
+ <li><a href="{{ next_url }}" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>
+ {% else %}
+ <li class="disabled"><a href="#" aria-label="Next"><span aria-hidden="true">&raquo;</span></a></li>
+ {% endif %}
+</ul>
diff --git a/rest_framework/templates/rest_framework/pagination/previous_and_next.html b/rest_framework/templates/rest_framework/pagination/previous_and_next.html
new file mode 100644
index 00000000..eacbfff4
--- /dev/null
+++ b/rest_framework/templates/rest_framework/pagination/previous_and_next.html
@@ -0,0 +1,12 @@
+<ul class="pager">
+{% if previous_url %}
+ <li class="previous"><a href="{{ previous_url }}">&laquo; Previous</a></li>
+{% else %}
+ <li class="previous disabled"><a href="#">&laquo; Previous</a></li>
+{% endif %}
+{% if next_url %}
+ <li class="next"><a href="{{ next_url }}">Next &raquo;</a></li>
+{% else %}
+ <li class="next disabled"><a href="#">Next &raquo;</li>
+{% endif %}
+</ul>
diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html
new file mode 100644
index 00000000..b4c9f1a1
--- /dev/null
+++ b/rest_framework/templates/rest_framework/raw_data_form.html
@@ -0,0 +1,12 @@
+{% load rest_framework %}
+{% csrf_token %}
+{{ form.non_field_errors }}
+{% for field in form %}
+ <div class="form-group">
+ {{ field.label_tag|add_class:"col-sm-2 control-label" }}
+ <div class="col-sm-10">
+ {{ field|add_class:"form-control" }}
+ <span class="help-block">{{ field.help_text }}</span>
+ </div>
+ </div>
+{% endfor %}
diff --git a/rest_framework/templates/rest_framework/vertical/checkbox.html b/rest_framework/templates/rest_framework/vertical/checkbox.html
new file mode 100644
index 00000000..e21a8e90
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/checkbox.html
@@ -0,0 +1,14 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="true" {% if value %}checked{% endif %}>
+ {% if field.label %}{{ field.label }}{% endif %}
+ </label>
+ </div>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div> \ No newline at end of file
diff --git a/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html
new file mode 100644
index 00000000..134cca66
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html
@@ -0,0 +1,30 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ {% if style.inline %}
+ <div>
+ {% for key, text in field.choices.items %}
+ <label class="checkbox-inline">
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ {% endfor %}
+ </div>
+ {% else %}
+ {% for key, text in field.choices.items %}
+ <div class="checkbox">
+ <label>
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templates/rest_framework/vertical/fieldset.html b/rest_framework/templates/rest_framework/vertical/fieldset.html
new file mode 100644
index 00000000..3eb5191c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/fieldset.html
@@ -0,0 +1,9 @@
+{% load rest_framework %}
+<fieldset>
+ {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
+ {% for nested_field in field %}
+ {% if not nested_field.read_only %}
+ {% render_field nested_field style=style %}
+ {% endif %}
+ {% endfor %}
+</fieldset>
diff --git a/rest_framework/templates/rest_framework/vertical/form.html b/rest_framework/templates/rest_framework/vertical/form.html
new file mode 100644
index 00000000..e68835c0
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/form.html
@@ -0,0 +1,11 @@
+{% load rest_framework %}
+<form role="form" action="." method="POST" novalidate>
+ {% csrf_token %}
+ {% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+ {% endfor %}
+ <!-- form.non_field_errors -->
+ <button type="submit" class="btn btn-default">Submit</button>
+</form>
diff --git a/rest_framework/templates/rest_framework/vertical/input.html b/rest_framework/templates/rest_framework/vertical/input.html
new file mode 100644
index 00000000..43cccd3e
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/input.html
@@ -0,0 +1,12 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ <input name="{{ field.name }}" {% if style.input_type != "file" %}class="form-control"{% endif %} type="{{ style.input_type }}" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if field.value %}value="{{ field.value }}"{% endif %}>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
new file mode 100644
index 00000000..82d7b5f4
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -0,0 +1,4 @@
+<fieldset>
+ {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
+ <p>Lists are not currently supported in HTML input.</p>
+</fieldset>
diff --git a/rest_framework/templates/rest_framework/vertical/radio.html b/rest_framework/templates/rest_framework/vertical/radio.html
new file mode 100644
index 00000000..ed9f9ddb
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/radio.html
@@ -0,0 +1,30 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ {% if style.inline %}
+ <div>
+ {% for key, text in field.choices.items %}
+ <label class="radio-inline">
+ <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ {% endfor %}
+ </div>
+ {% else %}
+ {% for key, text in field.choices.items %}
+ <div class="radio">
+ <label>
+ <input type="radio" name="{{ field.name }}" value="{{ key }}" {% if key == field.value %}checked{% endif %}>
+ {{ text }}
+ </label>
+ </div>
+ {% endfor %}
+ {% endif %}
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
new file mode 100644
index 00000000..1d1109f6
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -0,0 +1,19 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ <select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null or field.allow_blank %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
+ {% endfor %}
+ </select>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templates/rest_framework/vertical/select_multiple.html b/rest_framework/templates/rest_framework/vertical/select_multiple.html
new file mode 100644
index 00000000..81b25c2a
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/select_multiple.html
@@ -0,0 +1,21 @@
+{% load i18n %}
+{% trans "No items to select." as no_items %}
+
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ <select multiple {{ field.choices|yesno:",disabled" }} class="form-control" name="{{ field.name }}">
+ {% for key, text in field.choices.items %}
+ <option value="{{ key }}" {% if key in field.value %}selected{% endif %}>{{ text }}</option>
+ {% empty %}
+ <option>{{ no_items }}</option>
+ {% endfor %}
+ </select>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templates/rest_framework/vertical/textarea.html b/rest_framework/templates/rest_framework/vertical/textarea.html
new file mode 100644
index 00000000..840ea853
--- /dev/null
+++ b/rest_framework/templates/rest_framework/vertical/textarea.html
@@ -0,0 +1,12 @@
+<div class="form-group {% if field.errors %}has-error{% endif %}">
+ {% if field.label %}
+ <label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
+ {% endif %}
+ <textarea name="{{ field.name }}" class="form-control" {% if style.placeholder %}placeholder="{{ style.placeholder }}"{% endif %} {% if style.rows %}rows="{{ style.rows }}"{% endif %}>{% if field.value %}{{ field.value }}{% endif %}</textarea>
+ {% if field.errors %}
+ {% for error in field.errors %}<span class="help-block">{{ error }}</span>{% endfor %}
+ {% endif %}
+ {% if field.help_text %}
+ <span class="help-block">{{ field.help_text }}</span>
+ {% endif %}
+</div>
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index e9c1cdd5..bf0dc7b8 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -1,115 +1,32 @@
from __future__ import unicode_literals, absolute_import
from django import template
from django.core.urlresolvers import reverse, NoReverseMatch
-from django.http import QueryDict
+from django.utils import six
+from django.utils.encoding import iri_to_uri, force_text
from django.utils.html import escape
from django.utils.safestring import SafeData, mark_safe
-from rest_framework.compat import urlparse, force_text, six, smart_urlquote
-import re, string
+from django.utils.html import smart_urlquote
+from rest_framework.renderers import HTMLFormRenderer
+from rest_framework.utils.urls import replace_query_param
+import re
register = template.Library()
+# Regex for adding classes to html snippets
+class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
-# Note we don't use 'load staticfiles', because we need a 1.3 compatible
-# version, so instead we include the `static` template tag ourselves.
-
-# When 1.3 becomes unsupported by REST framework, we can instead start to
-# use the {% load staticfiles %} tag, remove the following code,
-# and add a dependency that `django.contrib.staticfiles` must be installed.
-
-# Note: We can't put this into the `compat` module because the compat import
-# from rest_framework.compat import ...
-# conflicts with this rest_framework template tag module.
-
-try: # Django 1.5+
- from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
-
- @register.tag('static')
- def do_static(parser, token):
- return StaticFilesNode.handle_token(parser, token)
-
-except ImportError:
- try: # Django 1.4
- from django.contrib.staticfiles.storage import staticfiles_storage
-
- @register.simple_tag
- def static(path):
- """
- A template tag that returns the URL to a file
- using staticfiles' storage backend
- """
- return staticfiles_storage.url(path)
-
- except ImportError: # Django 1.3
- from urlparse import urljoin
- from django import template
- from django.templatetags.static import PrefixNode
-
- class StaticNode(template.Node):
- def __init__(self, varname=None, path=None):
- if path is None:
- raise template.TemplateSyntaxError(
- "Static template nodes must be given a path to return.")
- self.path = path
- self.varname = varname
-
- def url(self, context):
- path = self.path.resolve(context)
- return self.handle_simple(path)
-
- def render(self, context):
- url = self.url(context)
- if self.varname is None:
- return url
- context[self.varname] = url
- return ''
-
- @classmethod
- def handle_simple(cls, path):
- return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
-
- @classmethod
- def handle_token(cls, parser, token):
- """
- Class method to parse prefix node and return a Node.
- """
- bits = token.split_contents()
-
- if len(bits) < 2:
- raise template.TemplateSyntaxError(
- "'%s' takes at least one argument (path to file)" % bits[0])
-
- path = parser.compile_filter(bits[1])
-
- if len(bits) >= 2 and bits[-2] == 'as':
- varname = bits[3]
- else:
- varname = None
-
- return cls(varname, path)
-
- @register.tag('static')
- def do_static_13(parser, token):
- return StaticNode.handle_token(parser, token)
-
-
-def replace_query_param(url, key, val):
- """
- Given a URL and a key/val pair, set or replace an item in the query
- parameters of the URL, and return the new URL.
- """
- (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
- query_dict = QueryDict(query).copy()
- query_dict[key] = val
- query = query_dict.urlencode()
- return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+@register.simple_tag
+def get_pagination_html(pager):
+ return pager.to_html()
-# Regex for adding classes to html snippets
-class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
+@register.simple_tag
+def render_field(field, style=None):
+ style = style or {}
+ renderer = style.get('renderer', HTMLFormRenderer())
+ return renderer.render_field(field, style)
-# And the template tags themselves...
@register.simple_tag
def optional_login(request):
@@ -121,22 +38,31 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
+ snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
return snippet
@register.simple_tag
-def optional_logout(request):
+def optional_logout(request, user):
"""
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
try:
logout_url = reverse('rest_framework:logout')
except NoReverseMatch:
- return ''
+ return '<li class="navbar-text">{user}</li>'.format(user=user)
- snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
- return snippet
+ snippet = """<li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ {user}
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href='{href}?next={next}'>Log out</a></li>
+ </ul>
+ </li>"""
+
+ return snippet.format(user=user, href=logout_url, next=escape(request.path))
@register.simple_tag
@@ -144,7 +70,9 @@ def add_query_param(request, key, val):
"""
Add a query parameter to the current request url, and return the new url.
"""
- return replace_query_param(request.get_full_path(), key, val)
+ iri = request.get_full_path()
+ uri = iri_to_uri(iri)
+ return escape(replace_query_param(uri, key, val))
@register.filter
@@ -177,7 +105,7 @@ def add_class(value, css_class):
# Bunch of stuff cloned from urlize
-TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
+TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"]
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('&lt;', '&gt;'),
('"', '"'), ("'", "'")]
word_split_re = re.compile(r'(\s+)')
@@ -186,6 +114,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net
simple_email_re = re.compile(r'^\S+@\S+\.\S+$')
+def smart_urlquote_wrapper(matched_url):
+ """
+ Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can
+ be raised here, see issue #1386
+ """
+ try:
+ return smart_urlquote(matched_url)
+ except ValueError:
+ return None
+
+
@register.filter
def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True):
"""
@@ -204,11 +143,12 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
If autoescape is True, the link text and URLs will get autoescaped.
"""
- trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+ def trim_url(x, limit=trim_url_limit):
+ return limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x
+
safe_input = isinstance(text, SafeData)
words = word_split_re.split(force_text(text))
for i, word in enumerate(words):
- match = None
if '.' in word or '@' in word or ':' in word:
# Deal with punctuation.
lead, middle, trail = '', word, ''
@@ -221,8 +161,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
middle = middle[len(opening):]
lead = lead + opening
# Keep parentheses at the end only if they're balanced.
- if (middle.endswith(closing)
- and middle.count(closing) == middle.count(opening) + 1):
+ if (
+ middle.endswith(closing) and
+ middle.count(closing) == middle.count(opening) + 1
+ ):
middle = middle[:-len(closing)]
trail = closing + trail
@@ -230,10 +172,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
url = None
nofollow_attr = ' rel="nofollow"' if nofollow else ''
if simple_url_re.match(middle):
- url = smart_urlquote(middle)
+ url = smart_urlquote_wrapper(middle)
elif simple_url_2_re.match(middle):
- url = smart_urlquote('http://%s' % middle)
- elif not ':' in middle and simple_email_re.match(middle):
+ url = smart_urlquote_wrapper('http://%s' % middle)
+ elif ':' not in middle and simple_email_re.match(middle):
local, domain = middle.rsplit('@', 1)
try:
domain = domain.encode('idna').decode('ascii')
diff --git a/rest_framework/test.py b/rest_framework/test.py
index a18f5a29..a83d082a 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -8,9 +8,11 @@ from django.conf import settings
from django.test.client import Client as DjangoClient
from django.test.client import ClientHandler
from django.test import testcases
+from django.utils import six
+from django.utils.http import urlencode
from rest_framework.settings import api_settings
from rest_framework.compat import RequestFactory as DjangoRequestFactory
-from rest_framework.compat import force_bytes_or_smart_bytes, six
+from rest_framework.compat import force_bytes_or_smart_bytes
def force_authenticate(request, user=None, token=None):
@@ -34,8 +36,8 @@ class APIRequestFactory(DjangoRequestFactory):
Encode the data returning a two tuple of (bytes, content_type)
"""
- if not data:
- return ('', None)
+ if data is None:
+ return ('', content_type)
assert format is None or content_type is None, (
'You may not set both `format` and `content_type`.'
@@ -48,9 +50,10 @@ class APIRequestFactory(DjangoRequestFactory):
else:
format = format or self.default_format
- assert format in self.renderer_classes, ("Invalid format '{0}'. "
- "Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
- "to enable extra request formats.".format(
+ assert format in self.renderer_classes, (
+ "Invalid format '{0}'. Available formats are {1}. "
+ "Set TEST_REQUEST_RENDERER_CLASSES to enable "
+ "extra request formats.".format(
format,
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
)
@@ -71,6 +74,17 @@ class APIRequestFactory(DjangoRequestFactory):
return ret, content_type
+ def get(self, path, data=None, **extra):
+ r = {
+ 'QUERY_STRING': urlencode(data or {}, doseq=True),
+ }
+ # Fix to support old behavior where you have the arguments in the url
+ # See #1461
+ if not data and '?' in path:
+ r['QUERY_STRING'] = path.split('?')[1]
+ r.update(extra)
+ return self.generic('GET', path, **r)
+
def post(self, path, data=None, format=None, content_type=None, **extra):
data, content_type = self._encode_data(data, format, content_type)
return self.generic('POST', path, data, content_type, **extra)
@@ -134,12 +148,70 @@ class APIClient(APIRequestFactory, DjangoClient):
"""
self.handler._force_user = user
self.handler._force_token = token
+ if user is None:
+ self.logout() # Also clear any possible session info if required
def request(self, **kwargs):
# Ensure that any credentials set get added to every request.
kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs)
+ def get(self, path, data=None, follow=False, **extra):
+ response = super(APIClient, self).get(path, data=data, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def post(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).post(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def put(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).put(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def patch(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).patch(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def delete(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).delete(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def options(self, path, data=None, format=None, content_type=None,
+ follow=False, **extra):
+ response = super(APIClient, self).options(
+ path, data=data, format=format, content_type=content_type, **extra)
+ if follow:
+ response = self._handle_redirects(response, **extra)
+ return response
+
+ def logout(self):
+ self._credentials = {}
+
+ # Also clear any `force_authenticate`
+ self.handler._force_user = None
+ self.handler._force_token = None
+
+ if self.session:
+ super(APIClient, self).logout()
+
class APITransactionTestCase(testcases.TransactionTestCase):
client_class = APIClient
diff --git a/rest_framework/tests/extras/bad_import.py b/rest_framework/tests/extras/bad_import.py
deleted file mode 100644
index 68263d94..00000000
--- a/rest_framework/tests/extras/bad_import.py
+++ /dev/null
@@ -1 +0,0 @@
-raise ValueError
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
deleted file mode 100644
index 1598ecd9..00000000
--- a/rest_framework/tests/models.py
+++ /dev/null
@@ -1,169 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
-
-
-def foobar():
- return 'foobar'
-
-
-class CustomField(models.CharField):
-
- def __init__(self, *args, **kwargs):
- kwargs['max_length'] = 12
- super(CustomField, self).__init__(*args, **kwargs)
-
-
-class RESTFrameworkModel(models.Model):
- """
- Base for test models that sets app_label, so they play nicely.
- """
- class Meta:
- app_label = 'tests'
- abstract = True
-
-
-class HasPositiveIntegerAsChoice(RESTFrameworkModel):
- some_choices = ((1, 'A'), (2, 'B'), (3, 'C'))
- some_integer = models.PositiveIntegerField(choices=some_choices)
-
-
-class Anchor(RESTFrameworkModel):
- text = models.CharField(max_length=100, default='anchor')
-
-
-class BasicModel(RESTFrameworkModel):
- text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
-
-
-class SlugBasedModel(RESTFrameworkModel):
- text = models.CharField(max_length=100)
- slug = models.SlugField(max_length=32)
-
-
-class DefaultValueModel(RESTFrameworkModel):
- text = models.CharField(default='foobar', max_length=100)
- extra = models.CharField(blank=True, null=True, max_length=100)
-
-
-class CallableDefaultValueModel(RESTFrameworkModel):
- text = models.CharField(default=foobar, max_length=100)
-
-
-class ManyToManyModel(RESTFrameworkModel):
- rel = models.ManyToManyField(Anchor, help_text='Some help text.')
-
-
-class ReadOnlyManyToManyModel(RESTFrameworkModel):
- text = models.CharField(max_length=100, default='anchor')
- rel = models.ManyToManyField(Anchor)
-
-
-# Model for regression test for #285
-
-class Comment(RESTFrameworkModel):
- email = models.EmailField()
- content = models.CharField(max_length=200)
- created = models.DateTimeField(auto_now_add=True)
-
-
-class ActionItem(RESTFrameworkModel):
- title = models.CharField(max_length=200)
- done = models.BooleanField(default=False)
- info = CustomField(default='---', max_length=12)
-
-
-# Models for reverse relations
-class Person(RESTFrameworkModel):
- name = models.CharField(max_length=10)
- age = models.IntegerField(null=True, blank=True)
-
- @property
- def info(self):
- return {
- 'name': self.name,
- 'age': self.age,
- }
-
-
-class BlogPost(RESTFrameworkModel):
- title = models.CharField(max_length=100)
- writer = models.ForeignKey(Person, null=True, blank=True)
-
- def get_first_comment(self):
- return self.blogpostcomment_set.all()[0]
-
-
-class BlogPostComment(RESTFrameworkModel):
- text = models.TextField()
- blog_post = models.ForeignKey(BlogPost)
-
-
-class Album(RESTFrameworkModel):
- title = models.CharField(max_length=100, unique=True)
-
-
-class Photo(RESTFrameworkModel):
- description = models.TextField()
- album = models.ForeignKey(Album)
-
-
-# Model for issue #324
-class BlankFieldModel(RESTFrameworkModel):
- title = models.CharField(max_length=100, blank=True, null=False)
-
-
-# Model for issue #380
-class OptionalRelationModel(RESTFrameworkModel):
- other = models.ForeignKey('OptionalRelationModel', blank=True, null=True)
-
-
-# Model for RegexField
-class Book(RESTFrameworkModel):
- isbn = models.CharField(max_length=13)
-
-
-# Models for relations tests
-# ManyToMany
-class ManyToManyTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class ManyToManySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
-
-
-# ForeignKey
-class ForeignKeyTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class ForeignKeySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
-
-
-# Nullable ForeignKey
-class NullableForeignKeySource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
-
-
-# OneToOne
-class OneToOneTarget(RESTFrameworkModel):
- name = models.CharField(max_length=100)
-
-
-class NullableOneToOneSource(RESTFrameworkModel):
- name = models.CharField(max_length=100)
- target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
- related_name='nullable_source')
-
-
-# Serializer used to test BasicModel
-class BasicModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
deleted file mode 100644
index a44813b6..00000000
--- a/rest_framework/tests/test_authentication.py
+++ /dev/null
@@ -1,635 +0,0 @@
-from __future__ import unicode_literals
-from django.contrib.auth.models import User
-from django.http import HttpResponse
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import HTTP_HEADER_ENCODING
-from rest_framework import exceptions
-from rest_framework import permissions
-from rest_framework import renderers
-from rest_framework.response import Response
-from rest_framework import status
-from rest_framework.authentication import (
- BaseAuthentication,
- TokenAuthentication,
- BasicAuthentication,
- SessionAuthentication,
- OAuthAuthentication,
- OAuth2Authentication
-)
-from rest_framework.authtoken.models import Token
-from rest_framework.compat import patterns, url, include
-from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope
-from rest_framework.compat import oauth, oauth_provider
-from rest_framework.test import APIRequestFactory, APIClient
-from rest_framework.views import APIView
-import base64
-import time
-import datetime
-
-factory = APIRequestFactory()
-
-
-class MockView(APIView):
- permission_classes = (permissions.IsAuthenticated,)
-
- def get(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
- def post(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
- def put(self, request):
- return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-
-
-urlpatterns = patterns('',
- (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
- (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
- (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
- (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
- (r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
- (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
- permission_classes=[permissions.TokenHasReadWriteScope]))
-)
-
-if oauth2_provider is not None:
- urlpatterns += patterns('',
- url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
- url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
- url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
- permission_classes=[permissions.TokenHasReadWriteScope])),
- )
-
-
-class BasicAuthTests(TestCase):
- """Basic authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- def test_post_form_passing_basic_auth(self):
- """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
- credentials = ('%s:%s' % (self.username, self.password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- auth = 'Basic %s' % base64_credentials
- response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_json_passing_basic_auth(self):
- """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
- credentials = ('%s:%s' % (self.username, self.password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- auth = 'Basic %s' % base64_credentials
- response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_failing_basic_auth(self):
- """Ensure POSTing form over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/basic/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_post_json_failing_basic_auth(self):
- """Ensure POSTing json over basic auth without correct credentials fails"""
- response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
- self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
-
-
-class SessionAuthTests(TestCase):
- """User session authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.non_csrf_client = APIClient(enforce_csrf_checks=False)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- def tearDown(self):
- self.csrf_client.logout()
-
- def test_post_form_session_auth_failing_csrf(self):
- """
- Ensure POSTing form over session authentication without CSRF token fails.
- """
- self.csrf_client.login(username=self.username, password=self.password)
- response = self.csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_post_form_session_auth_passing(self):
- """
- Ensure POSTing form over session authentication with logged in user and CSRF token passes.
- """
- self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_put_form_session_auth_passing(self):
- """
- Ensure PUTting form over session authentication with logged in user and CSRF token passes.
- """
- self.non_csrf_client.login(username=self.username, password=self.password)
- response = self.non_csrf_client.put('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_session_auth_failing(self):
- """
- Ensure POSTing form over session authentication without logged in user fails.
- """
- response = self.csrf_client.post('/session/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-
-class TokenAuthTests(TestCase):
- """Token authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.key = 'abcd1234'
- self.token = Token.objects.create(key=self.key, user=self.user)
-
- def test_post_form_passing_token_auth(self):
- """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
- auth = 'Token ' + self.key
- response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_json_passing_token_auth(self):
- """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
- auth = "Token " + self.key
- response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_post_form_failing_token_auth(self):
- """Ensure POSTing form over token auth without correct credentials fails"""
- response = self.csrf_client.post('/token/', {'example': 'example'})
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_post_json_failing_token_auth(self):
- """Ensure POSTing json over token auth without correct credentials fails"""
- response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
- self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
-
- def test_token_has_auto_assigned_key_if_none_provided(self):
- """Ensure creating a token with no key will auto-assign a key"""
- self.token.delete()
- token = Token.objects.create(user=self.user)
- self.assertTrue(bool(token.key))
-
- def test_token_login_json(self):
- """Ensure token login view using JSON POST works."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': self.password}, format='json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['token'], self.key)
-
- def test_token_login_json_bad_creds(self):
- """Ensure token login view using JSON POST fails if bad credentials are used."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': "badpass"}, format='json')
- self.assertEqual(response.status_code, 400)
-
- def test_token_login_json_missing_fields(self):
- """Ensure token login view using JSON POST fails if missing fields."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username}, format='json')
- self.assertEqual(response.status_code, 400)
-
- def test_token_login_form(self):
- """Ensure token login view using form POST works."""
- client = APIClient(enforce_csrf_checks=True)
- response = client.post('/auth-token/',
- {'username': self.username, 'password': self.password})
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['token'], self.key)
-
-
-class IncorrectCredentialsTests(TestCase):
- def test_incorrect_credentials(self):
- """
- If a request contains bad authentication credentials, then
- authentication should run and error, even if no permissions
- are set on the view.
- """
- class IncorrectCredentialsAuth(BaseAuthentication):
- def authenticate(self, request):
- raise exceptions.AuthenticationFailed('Bad credentials')
-
- request = factory.get('/')
- view = MockView.as_view(
- authentication_classes=(IncorrectCredentialsAuth,),
- permission_classes=()
- )
- response = view(request)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- self.assertEqual(response.data, {'detail': 'Bad credentials'})
-
-
-class OAuthTests(TestCase):
- """OAuth 1.0a authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- # these imports are here because oauth is optional and hiding them in try..except block or compat
- # could obscure problems if something breaks
- from oauth_provider.models import Consumer, Resource
- from oauth_provider.models import Token as OAuthToken
- from oauth_provider import consts
-
- self.consts = consts
-
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CONSUMER_KEY = 'consumer_key'
- self.CONSUMER_SECRET = 'consumer_secret'
- self.TOKEN_KEY = "token_key"
- self.TOKEN_SECRET = "token_secret"
-
- self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
- name='example', user=self.user, status=self.consts.ACCEPTED)
-
- self.resource = Resource.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, resource=self.resource,
- token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
- )
-
- def _create_authorization_header(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
-
- return req.to_header()["Authorization"]
-
- def _create_authorization_url_parameters(self):
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="GET", url="http://example.com", parameters=params)
-
- signature_method = oauth.SignatureMethod_PLAINTEXT()
- req.sign_request(signature_method, self.consumer, self.token)
- return dict(req)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_passing_oauth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_repeated_nonce_failing_oauth(self):
- """Ensure POSTing form over OAuth with repeated auth (same nonces and timestamp) credentials fails"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- # simulate reply attack auth header containes already used (nonce, timestamp) pair
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_token_removed_failing_oauth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_consumer_status_not_accepted_failing_oauth(self):
- """Ensure POSTing when consumer status is anything other than ACCEPTED fails"""
- for consumer_status in (self.consts.CANCELED, self.consts.PENDING, self.consts.REJECTED):
- self.consumer.status = consumer_status
- self.consumer.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_request_token_failing_oauth(self):
- """Ensure POSTing with unauthorized request token instead of access token fails"""
- self.token.token_type = self.token.REQUEST
- self.token.save()
-
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_urlencoded_parameters(self):
- """Ensure POSTing with x-www-form-urlencoded auth parameters passes"""
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_url_parameters(self):
- """Ensure GETing with auth in url parameters passes"""
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_hmac_sha1_signature_passes(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_get_form_with_readonly_resource_passing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.get('/oauth-with-scope/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_readonly_resource_failing_auth(self):
- """Ensure POSTing with a readonly resource instead of a write scope fails"""
- read_only_access_token = self.token
- read_only_access_token.resource.is_readonly = True
- read_only_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_post_form_with_write_resource_passing_auth(self):
- """Ensure POSTing with a write resource succeed"""
- read_write_access_token = self.token
- read_write_access_token.resource.is_readonly = False
- read_write_access_token.resource.save()
- params = self._create_authorization_url_parameters()
- response = self.csrf_client.post('/oauth-with-scope/', params)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_consumer_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': self.token.key,
- 'oauth_consumer_key': 'badconsumerkey'
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth_provider, 'django-oauth-plus not installed')
- @unittest.skipUnless(oauth, 'oauth2 not installed')
- def test_bad_token_key(self):
- """Ensure POSTing using HMAC_SHA1 signature method passes"""
- params = {
- 'oauth_version': "1.0",
- 'oauth_nonce': oauth.generate_nonce(),
- 'oauth_timestamp': int(time.time()),
- 'oauth_token': 'badtokenkey',
- 'oauth_consumer_key': self.consumer.key
- }
-
- req = oauth.Request(method="POST", url="http://testserver/oauth/", parameters=params)
-
- signature_method = oauth.SignatureMethod_HMAC_SHA1()
- req.sign_request(signature_method, self.consumer, self.token)
- auth = req.to_header()["Authorization"]
-
- response = self.csrf_client.post('/oauth/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
-
-class OAuth2Tests(TestCase):
- """OAuth 2.0 authentication"""
- urls = 'rest_framework.tests.test_authentication'
-
- def setUp(self):
- self.csrf_client = APIClient(enforce_csrf_checks=True)
- self.username = 'john'
- self.email = 'lennon@thebeatles.com'
- self.password = 'password'
- self.user = User.objects.create_user(self.username, self.email, self.password)
-
- self.CLIENT_ID = 'client_key'
- self.CLIENT_SECRET = 'client_secret'
- self.ACCESS_TOKEN = "access_token"
- self.REFRESH_TOKEN = "refresh_token"
-
- self.oauth2_client = oauth2_provider_models.Client.objects.create(
- client_id=self.CLIENT_ID,
- client_secret=self.CLIENT_SECRET,
- redirect_uri='',
- client_type=0,
- name='example',
- user=None,
- )
-
- self.access_token = oauth2_provider_models.AccessToken.objects.create(
- token=self.ACCESS_TOKEN,
- client=self.oauth2_client,
- user=self.user,
- )
- self.refresh_token = oauth2_provider_models.RefreshToken.objects.create(
- user=self.user,
- access_token=self.access_token,
- client=self.oauth2_client
- )
-
- def _create_authorization_header(self, token=None):
- return "Bearer {0}".format(token or self.access_token.token)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_type_failing(self):
- """Ensure that a wrong token type lead to the correct HTTP error status code"""
- auth = "Wrong token-type-obsviously"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_format_failing(self):
- """Ensure that a wrong token format lead to the correct HTTP error status code"""
- auth = "Bearer wrong token format"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_with_wrong_authorization_header_token_failing(self):
- """Ensure that a wrong token lead to the correct HTTP error status code"""
- auth = "Bearer wrong-token"
- response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 401)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_get_form_passing_auth(self):
- """Ensure GETing form over OAuth with correct client credentials succeed"""
- auth = self._create_authorization_header()
- response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_passing_auth(self):
- """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF"""
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_token_removed_failing_auth(self):
- """Ensure POSTing when there is no OAuth access token in db fails"""
- self.access_token.delete()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_refresh_token_failing_auth(self):
- """Ensure POSTing with refresh token instead of access token fails"""
- auth = self._create_authorization_header(token=self.refresh_token.token)
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_expired_access_token_failing_auth(self):
- """Ensure POSTing with expired access token fails with an 'Invalid token' error"""
- self.access_token.expires = datetime.datetime.now() - datetime.timedelta(seconds=10) # 10 seconds late
- self.access_token.save()
- auth = self._create_authorization_header()
- response = self.csrf_client.post('/oauth2-test/', HTTP_AUTHORIZATION=auth)
- self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
- self.assertIn('Invalid token', response.content)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_invalid_scope_failing_auth(self):
- """Ensure POSTing with a readonly scope instead of a write scope fails"""
- read_only_access_token = self.access_token
- read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read']
- read_only_access_token.save()
- auth = self._create_authorization_header(token=read_only_access_token.token)
- response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
- def test_post_form_with_valid_scope_passing_auth(self):
- """Ensure POSTing with a write scope succeed"""
- read_write_access_token = self.access_token
- read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write']
- read_write_access_token.save()
- auth = self._create_authorization_header(token=read_write_access_token.token)
- response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
- self.assertEqual(response.status_code, 200)
-
-
-class FailingAuthAccessedInRenderer(TestCase):
- def setUp(self):
- class AuthAccessingRenderer(renderers.BaseRenderer):
- media_type = 'text/plain'
- format = 'txt'
-
- def render(self, data, media_type=None, renderer_context=None):
- request = renderer_context['request']
- if request.user.is_authenticated():
- return b'authenticated'
- return b'not authenticated'
-
- class FailingAuth(BaseAuthentication):
- def authenticate(self, request):
- raise exceptions.AuthenticationFailed('authentication failed')
-
- class ExampleView(APIView):
- authentication_classes = (FailingAuth,)
- renderer_classes = (AuthAccessingRenderer,)
-
- def get(self, request):
- return Response({'foo': 'bar'})
-
- self.view = ExampleView.as_view()
-
- def test_failing_auth_accessed_in_renderer(self):
- """
- When authentication fails the renderer should still be able to access
- `request.user` without raising an exception. Particularly relevant
- to HTML responses that might reasonably access `request.user`.
- """
- request = factory.get('/')
- response = self.view(request)
- content = response.render().content
- self.assertEqual(content, b'not authenticated')
diff --git a/rest_framework/tests/test_breadcrumbs.py b/rest_framework/tests/test_breadcrumbs.py
deleted file mode 100644
index 41ddf2ce..00000000
--- a/rest_framework/tests/test_breadcrumbs.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework.compat import patterns, url
-from rest_framework.utils.breadcrumbs import get_breadcrumbs
-from rest_framework.views import APIView
-
-
-class Root(APIView):
- pass
-
-
-class ResourceRoot(APIView):
- pass
-
-
-class ResourceInstance(APIView):
- pass
-
-
-class NestedResourceRoot(APIView):
- pass
-
-
-class NestedResourceInstance(APIView):
- pass
-
-urlpatterns = patterns('',
- url(r'^$', Root.as_view()),
- url(r'^resource/$', ResourceRoot.as_view()),
- url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
- url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
- url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
-)
-
-
-class BreadcrumbTests(TestCase):
- """Tests the breadcrumb functionality used by the HTML renderer."""
-
- urls = 'rest_framework.tests.test_breadcrumbs'
-
- def test_root_breadcrumbs(self):
- url = '/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
-
- def test_resource_root_breadcrumbs(self):
- url = '/resource/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/')])
-
- def test_resource_instance_breadcrumbs(self):
- url = '/resource/123'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123')])
-
- def test_nested_resource_breadcrumbs(self):
- url = '/resource/123/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/')])
-
- def test_nested_resource_instance_breadcrumbs(self):
- url = '/resource/123/abc'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/'),
- ('Nested Resource Instance', '/resource/123/abc')])
-
- def test_broken_url_breadcrumbs_handled_gracefully(self):
- url = '/foobar'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
deleted file mode 100644
index 6836ec86..00000000
--- a/rest_framework/tests/test_fields.py
+++ /dev/null
@@ -1,898 +0,0 @@
-"""
-General serializer field tests.
-"""
-from __future__ import unicode_literals
-
-import datetime
-from decimal import Decimal
-from uuid import uuid4
-from django.core import validators
-from django.db import models
-from django.test import TestCase
-from django.utils.datastructures import SortedDict
-from rest_framework import serializers
-from rest_framework.tests.models import RESTFrameworkModel
-
-
-class TimestampedModel(models.Model):
- added = models.DateTimeField(auto_now_add=True)
- updated = models.DateTimeField(auto_now=True)
-
-
-class CharPrimaryKeyModel(models.Model):
- id = models.CharField(max_length=20, primary_key=True)
-
-
-class TimestampedModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimestampedModel
-
-
-class CharPrimaryKeyModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = CharPrimaryKeyModel
-
-
-class TimeFieldModel(models.Model):
- clock = models.TimeField()
-
-
-class TimeFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = TimeFieldModel
-
-
-class BasicFieldTests(TestCase):
- def test_auto_now_fields_read_only(self):
- """
- auto_now and auto_now_add fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['added'].read_only, True)
-
- def test_auto_pk_fields_read_only(self):
- """
- AutoField fields should be read_only by default.
- """
- serializer = TimestampedModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, True)
-
- def test_non_auto_pk_fields_not_read_only(self):
- """
- PK fields other than AutoField fields should not be read_only by default.
- """
- serializer = CharPrimaryKeyModelSerializer()
- self.assertEqual(serializer.fields['id'].read_only, False)
-
- def test_dict_field_ordering(self):
- """
- Field should preserve dictionary ordering, if it exists.
- See: https://github.com/tomchristie/django-rest-framework/issues/832
- """
- ret = SortedDict()
- ret['c'] = 1
- ret['b'] = 1
- ret['a'] = 1
- ret['z'] = 1
- field = serializers.Field()
- keys = list(field.to_native(ret).keys())
- self.assertEqual(keys, ['c', 'b', 'a', 'z'])
-
-
-class DateFieldTest(TestCase):
- """
- Tests for the DateFieldTest from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateField()
- result_1 = f.from_native('1984-07-31')
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_from_native_datetime_date(self):
- """
- Make sure from_native() accepts a datetime.date instance.
- """
- f = serializers.DateField()
- result_1 = f.from_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(result_1, datetime.date(1984, 7, 31))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
- result = f.from_native('1984 -- 31')
-
- self.assertEqual(datetime.date(1984, 1, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateField(input_formats=['%Y -- %d'])
-
- try:
- f.from_native('1984-07-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY -- DD"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_date(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid date.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984-13-31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateField()
-
- try:
- f.from_native('1984 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns datetime as default.
- """
- f = serializers.DateField()
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual(datetime.date(1984, 7, 31), result_1)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with 'iso-8601' returns iso formated date.
- """
- f = serializers.DateField(format='iso-8601')
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984-07-31', result_1)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateField(format="%Y - %m.%d")
-
- result_1 = f.to_native(datetime.date(1984, 7, 31))
-
- self.assertEqual('1984 - 07.31', result_1)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class DateTimeFieldTest(TestCase):
- """
- Tests for the DateTimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native('1984-07-31 04:31')
- result_2 = f.from_native('1984-07-31 04:31:59')
- result_3 = f.from_native('1984-07-31 04:31:59.000200')
-
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_datetime(self):
- """
- Make sure from_native() accepts a datetime.datetime instance.
- """
- f = serializers.DateTimeField()
- result_1 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_2 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_3 = f.from_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.datetime(1984, 7, 31, 4, 31))
- self.assertEqual(result_2, datetime.datetime(1984, 7, 31, 4, 31, 59))
- self.assertEqual(result_3, datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
- result = f.from_native('1984 -- 04:59')
-
- self.assertEqual(datetime.datetime(1984, 1, 1, 4, 59), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.DateTimeField(input_formats=['%Y -- %H:%M'])
-
- try:
- f.from_native('1984-07-31 04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: YYYY -- hh:mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DateTimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_datetime(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid datetime.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.DateTimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns isoformat as default.
- """
- f = serializers.DateTimeField()
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual(datetime.datetime(1984, 7, 31), result_1)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31), result_2)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59), result_3)
- self.assertEqual(datetime.datetime(1984, 7, 31, 4, 31, 59, 200), result_4)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format=iso-8601 returns iso formatted datetime.
- """
- f = serializers.DateTimeField(format='iso-8601')
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984-07-31T00:00:00', result_1)
- self.assertEqual('1984-07-31T04:31:00', result_2)
- self.assertEqual('1984-07-31T04:31:59', result_3)
- self.assertEqual('1984-07-31T04:31:59.000200', result_4)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.DateTimeField(format="%Y - %H:%M")
-
- result_1 = f.to_native(datetime.datetime(1984, 7, 31))
- result_2 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31))
- result_3 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59))
- result_4 = f.to_native(datetime.datetime(1984, 7, 31, 4, 31, 59, 200))
-
- self.assertEqual('1984 - 00:00', result_1)
- self.assertEqual('1984 - 04:31', result_2)
- self.assertEqual('1984 - 04:31', result_3)
- self.assertEqual('1984 - 04:31', result_4)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DateTimeField(required=False)
- self.assertEqual(None, f.to_native(None))
-
-
-class TimeFieldTest(TestCase):
- """
- Tests for the TimeField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts default iso input formats.
- """
- f = serializers.TimeField()
- result_1 = f.from_native('04:31')
- result_2 = f.from_native('04:31:59')
- result_3 = f.from_native('04:31:59.000200')
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_from_native_datetime_time(self):
- """
- Make sure from_native() accepts a datetime.time instance.
- """
- f = serializers.TimeField()
- result_1 = f.from_native(datetime.time(4, 31))
- result_2 = f.from_native(datetime.time(4, 31, 59))
- result_3 = f.from_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(result_1, datetime.time(4, 31))
- self.assertEqual(result_2, datetime.time(4, 31, 59))
- self.assertEqual(result_3, datetime.time(4, 31, 59, 200))
-
- def test_from_native_custom_format(self):
- """
- Make sure from_native() accepts custom input formats.
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
- result = f.from_native('04 -- 31')
-
- self.assertEqual(datetime.time(4, 31), result)
-
- def test_from_native_invalid_default_on_custom_format(self):
- """
- Make sure from_native() don't accept default formats if custom format is preset
- """
- f = serializers.TimeField(input_formats=['%H -- %M'])
-
- try:
- f.from_native('04:31:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: hh -- mm"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.TimeField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.TimeField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_from_native_invalid_time(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid time.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04:61:59')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_invalid_format(self):
- """
- Make sure from_native() raises a ValidationError on passing an invalid format.
- """
- f = serializers.TimeField()
-
- try:
- f.from_native('04 -- 31')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Time has wrong format. Use one of these formats instead: "
- "hh:mm[:ss[.uuuuuu]]"])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_to_native(self):
- """
- Make sure to_native() returns time object as default.
- """
- f = serializers.TimeField()
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual(datetime.time(4, 31), result_1)
- self.assertEqual(datetime.time(4, 31, 59), result_2)
- self.assertEqual(datetime.time(4, 31, 59, 200), result_3)
-
- def test_to_native_iso(self):
- """
- Make sure to_native() with format='iso-8601' returns iso formatted time.
- """
- f = serializers.TimeField(format='iso-8601')
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04:31:00', result_1)
- self.assertEqual('04:31:59', result_2)
- self.assertEqual('04:31:59.000200', result_3)
-
- def test_to_native_custom_format(self):
- """
- Make sure to_native() returns correct custom format.
- """
- f = serializers.TimeField(format="%H - %S [%f]")
- result_1 = f.to_native(datetime.time(4, 31))
- result_2 = f.to_native(datetime.time(4, 31, 59))
- result_3 = f.to_native(datetime.time(4, 31, 59, 200))
-
- self.assertEqual('04 - 00 [000000]', result_1)
- self.assertEqual('04 - 59 [000000]', result_2)
- self.assertEqual('04 - 59 [000200]', result_3)
-
-
-class DecimalFieldTest(TestCase):
- """
- Tests for the DecimalField from_native() and to_native() behavior
- """
-
- def test_from_native_string(self):
- """
- Make sure from_native() accepts string values
- """
- f = serializers.DecimalField()
- result_1 = f.from_native('9000')
- result_2 = f.from_native('1.00000001')
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_from_native_invalid_string(self):
- """
- Make sure from_native() raises ValidationError on passing invalid string
- """
- f = serializers.DecimalField()
-
- try:
- f.from_native('123.45.6')
- except validators.ValidationError as e:
- self.assertEqual(e.messages, ["Enter a number."])
- else:
- self.fail("ValidationError was not properly raised")
-
- def test_from_native_integer(self):
- """
- Make sure from_native() accepts integer values
- """
- f = serializers.DecimalField()
- result = f.from_native(9000)
-
- self.assertEqual(Decimal('9000'), result)
-
- def test_from_native_float(self):
- """
- Make sure from_native() accepts float values
- """
- f = serializers.DecimalField()
- result = f.from_native(1.00000001)
-
- self.assertEqual(Decimal('1.00000001'), result)
-
- def test_from_native_empty(self):
- """
- Make sure from_native() returns None on empty param.
- """
- f = serializers.DecimalField()
- result = f.from_native('')
-
- self.assertEqual(result, None)
-
- def test_from_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField()
- result = f.from_native(None)
-
- self.assertEqual(result, None)
-
- def test_to_native(self):
- """
- Make sure to_native() returns Decimal as string.
- """
- f = serializers.DecimalField()
-
- result_1 = f.to_native(Decimal('9000'))
- result_2 = f.to_native(Decimal('1.00000001'))
-
- self.assertEqual(Decimal('9000'), result_1)
- self.assertEqual(Decimal('1.00000001'), result_2)
-
- def test_to_native_none(self):
- """
- Make sure from_native() returns None on None param.
- """
- f = serializers.DecimalField(required=False)
- self.assertEqual(None, f.to_native(None))
-
- def test_valid_serialization(self):
- """
- Make sure the serializer works correctly
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=9010,
- min_value=9000,
- max_digits=6,
- decimal_places=2)
-
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.2'}).is_valid())
- self.assertTrue(DecimalSerializer(data={'decimal_field': '9001.23'}).is_valid())
-
- self.assertFalse(DecimalSerializer(data={'decimal_field': '8000'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9900'}).is_valid())
- self.assertFalse(DecimalSerializer(data={'decimal_field': '9001.234'}).is_valid())
-
- def test_raise_max_value(self):
- """
- Make sure max_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '123'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
-
- def test_raise_min_value(self):
- """
- Make sure min_value violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(min_value=100)
-
- s = DecimalSerializer(data={'decimal_field': '99'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
-
- def test_raise_max_digits(self):
- """
- Make sure max_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=5)
-
- s = DecimalSerializer(data={'decimal_field': '123.456'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
-
- def test_raise_max_decimal_places(self):
- """
- Make sure max_decimal_places violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '123.4567'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
-
- def test_raise_max_whole_digits(self):
- """
- Make sure max_whole_digits violations raises ValidationError
- """
- class DecimalSerializer(serializers.Serializer):
- decimal_field = serializers.DecimalField(max_digits=4, decimal_places=3)
-
- s = DecimalSerializer(data={'decimal_field': '12345.6'})
-
- self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
-
-
-class ChoiceFieldTests(TestCase):
- """
- Tests for the ChoiceField options generator
- """
-
- SAMPLE_CHOICES = [
- ('red', 'Red'),
- ('green', 'Green'),
- ('blue', 'Blue'),
- ]
-
- def test_choices_required(self):
- """
- Make sure proper choices are rendered if field is required
- """
- f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, self.SAMPLE_CHOICES)
-
- def test_choices_not_required(self):
- """
- Make sure proper choices (plus blank) are rendered if the field isn't required
- """
- f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES)
- self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
-
-
-class EmailFieldTests(TestCase):
- """
- Tests for EmailField attribute values
- """
-
- class EmailFieldModel(RESTFrameworkModel):
- email_field = models.EmailField(blank=True)
-
- class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- email_field = models.EmailField(max_length=150, blank=True)
-
- def test_default_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
-
- def test_given_model_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.EmailFieldWithGivenMaxLengthModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
-
- def test_given_serializer_value(self):
- class EmailFieldSerializer(serializers.ModelSerializer):
- email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
-
- class Meta:
- model = self.EmailFieldModel
-
- serializer = EmailFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
-
-
-class SlugFieldTests(TestCase):
- """
- Tests for SlugField attribute values
- """
-
- class SlugFieldModel(RESTFrameworkModel):
- slug_field = models.SlugField(blank=True)
-
- class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- slug_field = models.SlugField(max_length=84, blank=True)
-
- def test_default_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
-
- def test_given_model_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.SlugFieldWithGivenMaxLengthModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
-
- def test_given_serializer_value(self):
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field',
- max_length=20, required=False)
-
- class Meta:
- model = self.SlugFieldModel
-
- serializer = SlugFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['slug_field'],
- 'max_length'), 20)
-
- def test_invalid_slug(self):
- """
- Make sure an invalid slug raises ValidationError
- """
- class SlugFieldSerializer(serializers.ModelSerializer):
- slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
-
- class Meta:
- model = self.SlugFieldModel
-
- s = SlugFieldSerializer(data={'slug_field': 'a b'})
-
- self.assertEqual(s.is_valid(), False)
- self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
-
-
-class URLFieldTests(TestCase):
- """
- Tests for URLField attribute values
- """
-
- class URLFieldModel(RESTFrameworkModel):
- url_field = models.URLField(blank=True)
-
- class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
- url_field = models.URLField(max_length=128, blank=True)
-
- def test_default_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 200)
-
- def test_given_model_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 128)
-
- def test_given_serializer_value(self):
- class URLFieldSerializer(serializers.ModelSerializer):
- url_field = serializers.URLField(source='url_field',
- max_length=20, required=False)
-
- class Meta:
- model = self.URLFieldWithGivenMaxLengthModel
-
- serializer = URLFieldSerializer(data={})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(getattr(serializer.fields['url_field'],
- 'max_length'), 20)
-
-
-class FieldMetadata(TestCase):
- def setUp(self):
- self.required_field = serializers.Field()
- self.required_field.label = uuid4().hex
- self.required_field.required = True
-
- self.optional_field = serializers.Field()
- self.optional_field.label = uuid4().hex
- self.optional_field.required = False
-
- def test_required(self):
- self.assertEqual(self.required_field.metadata()['required'], True)
-
- def test_optional(self):
- self.assertEqual(self.optional_field.metadata()['required'], False)
-
- def test_label(self):
- for field in (self.required_field, self.optional_field):
- self.assertEqual(field.metadata()['label'], field.label)
-
-
-class FieldCallableDefault(TestCase):
- def setUp(self):
- self.simple_callable = lambda: 'foo bar'
-
- def test_default_can_be_simple_callable(self):
- """
- Ensure that the 'default' argument can also be a simple callable.
- """
- field = serializers.WritableField(default=self.simple_callable)
- into = {}
- field.field_from_native({}, {}, 'field', into)
- self.assertEqual(into, {'field': 'foo bar'})
-
-
-class CustomIntegerField(TestCase):
- """
- Test that custom fields apply min_value and max_value constraints
- """
- def test_custom_fields_can_be_validated_for_value(self):
-
- class MoneyField(models.PositiveIntegerField):
- pass
-
- class EntryModel(models.Model):
- bank = MoneyField(validators=[validators.MaxValueValidator(100)])
-
- class EntrySerializer(serializers.ModelSerializer):
- class Meta:
- model = EntryModel
-
- entry = EntryModel(bank=1)
-
- serializer = EntrySerializer(entry, data={"bank": 11})
- self.assertTrue(serializer.is_valid())
-
- serializer = EntrySerializer(entry, data={"bank": -1})
- self.assertFalse(serializer.is_valid())
-
- serializer = EntrySerializer(entry, data={"bank": 101})
- self.assertFalse(serializer.is_valid())
-
-
diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py
deleted file mode 100644
index 487046ac..00000000
--- a/rest_framework/tests/test_files.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.compat import BytesIO
-from rest_framework.compat import six
-import datetime
-
-
-class UploadedFile(object):
- def __init__(self, file, created=None):
- self.file = file
- self.created = created or datetime.datetime.now()
-
-
-class UploadedFileSerializer(serializers.Serializer):
- file = serializers.FileField()
- created = serializers.DateTimeField()
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.file = attrs['file']
- instance.created = attrs['created']
- return instance
- return UploadedFile(**attrs)
-
-
-class FileSerializerTests(TestCase):
- def test_create(self):
- now = datetime.datetime.now()
- file = BytesIO(six.b('stuff'))
- file.name = 'stuff.txt'
- file.size = len(file.getvalue())
- serializer = UploadedFileSerializer(data={'created': now}, files={'file': file})
- uploaded_file = UploadedFile(file=file, created=now)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.object.created, uploaded_file.created)
- self.assertEqual(serializer.object.file, uploaded_file.file)
- self.assertFalse(serializer.object is uploaded_file)
-
- def test_creation_failure(self):
- """
- Passing files=None should result in an ValidationError
-
- Regression test for:
- https://github.com/tomchristie/django-rest-framework/issues/542
- """
- now = datetime.datetime.now()
-
- serializer = UploadedFileSerializer(data={'created': now})
- self.assertFalse(serializer.is_valid())
- self.assertIn('file', serializer.errors)
diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py
deleted file mode 100644
index c9d9e7ff..00000000
--- a/rest_framework/tests/test_filters.py
+++ /dev/null
@@ -1,474 +0,0 @@
-from __future__ import unicode_literals
-import datetime
-from decimal import Decimal
-from django.db import models
-from django.core.urlresolvers import reverse
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import generics, serializers, status, filters
-from rest_framework.compat import django_filters, patterns, url
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel
-
-factory = APIRequestFactory()
-
-
-class FilterableItem(models.Model):
- text = models.CharField(max_length=100)
- decimal = models.DecimalField(max_digits=4, decimal_places=2)
- date = models.DateField()
-
-
-if django_filters:
- # Basic filter on a list view.
- class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_fields = ['decimal', 'date']
- filter_backends = (filters.DjangoFilterBackend,)
-
- # These class are used to test a filter class.
- class SeveralFieldsFilter(django_filters.FilterSet):
- text = django_filters.CharFilter(lookup_type='icontains')
- decimal = django_filters.NumberFilter(lookup_type='lt')
- date = django_filters.DateFilter(lookup_type='gt')
-
- class Meta:
- model = FilterableItem
- fields = ['text', 'decimal', 'date']
-
- class FilterClassRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- # These classes are used to test a misconfigured filter class.
- class MisconfiguredFilter(django_filters.FilterSet):
- text = django_filters.CharFilter(lookup_type='icontains')
-
- class Meta:
- model = BasicModel
- fields = ['text']
-
- class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
- model = FilterableItem
- filter_class = MisconfiguredFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- class FilterClassDetailView(generics.RetrieveAPIView):
- model = FilterableItem
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- # Regression test for #814
- class FilterableItemSerializer(serializers.ModelSerializer):
- class Meta:
- model = FilterableItem
-
- class FilterFieldsQuerysetView(generics.ListCreateAPIView):
- queryset = FilterableItem.objects.all()
- serializer_class = FilterableItemSerializer
- filter_fields = ['decimal', 'date']
- filter_backends = (filters.DjangoFilterBackend,)
-
- class GetQuerysetView(generics.ListCreateAPIView):
- serializer_class = FilterableItemSerializer
- filter_class = SeveralFieldsFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- def get_queryset(self):
- return FilterableItem.objects.all()
-
- urlpatterns = patterns('',
- url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
- url(r'^$', FilterClassRootView.as_view(), name='root-view'),
- url(r'^get-queryset/$', GetQuerysetView.as_view(),
- name='get-queryset-view'),
- )
-
-
-class CommonFilteringTestCase(TestCase):
- def _serialize_object(self, obj):
- return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
-
- def setUp(self):
- """
- Create 10 FilterableItem instances.
- """
- base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
- for i in range(10):
- text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
- decimal = base_data[1] + i
- date = base_data[2] - datetime.timedelta(days=i * 2)
- FilterableItem(text=text, decimal=decimal, date=date).save()
-
- self.objects = FilterableItem.objects
- self.data = [
- self._serialize_object(obj)
- for obj in self.objects.all()
- ]
-
-
-class IntegrationTestFiltering(CommonFilteringTestCase):
- """
- Integration tests for filtered list views.
- """
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_fields_root_view(self):
- """
- GET requests to paginated ListCreateAPIView should return paginated results.
- """
- view = FilterFieldsRootView.as_view()
-
- # Basic test with no filter.
- request = factory.get('/')
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- # Tests that the decimal filter works.
- search_decimal = Decimal('2.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the date filter works.
- search_date = datetime.date(2012, 9, 22)
- request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22'
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] == search_date]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_filter_with_queryset(self):
- """
- Regression test for #814.
- """
- view = FilterFieldsQuerysetView.as_view()
-
- # Tests that the decimal filter works.
- search_decimal = Decimal('2.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] == search_decimal]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_filter_with_get_queryset_only(self):
- """
- Regression test for #834.
- """
- view = GetQuerysetView.as_view()
- request = factory.get('/get-queryset/')
- view(request).render()
- # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
- # here when neither `model' nor `queryset' was specified.
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_class_root_view(self):
- """
- GET requests to filtered ListCreateAPIView that have a filter_class set
- should return filtered results.
- """
- view = FilterClassRootView.as_view()
-
- # Basic test with no filter.
- request = factory.get('/')
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- # Tests that the decimal filter set with 'lt' in the filter class works.
- search_decimal = Decimal('4.25')
- request = factory.get('/?decimal=%s' % search_decimal)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['decimal'] < search_decimal]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the date filter set with 'gt' in the filter class works.
- search_date = datetime.date(2012, 10, 2)
- request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02'
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date]
- self.assertEqual(response.data, expected_data)
-
- # Tests that the text filter set with 'icontains' in the filter class works.
- search_text = 'ff'
- request = factory.get('/?text=%s' % search_text)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if search_text in f['text'].lower()]
- self.assertEqual(response.data, expected_data)
-
- # Tests that multiple filters works.
- search_decimal = Decimal('5.25')
- search_date = datetime.date(2012, 10, 2)
- request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date))
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- expected_data = [f for f in self.data if f['date'] > search_date and
- f['decimal'] < search_decimal]
- self.assertEqual(response.data, expected_data)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_incorrectly_configured_filter(self):
- """
- An error should be displayed when the filter class is misconfigured.
- """
- view = IncorrectlyConfiguredRootView.as_view()
-
- request = factory.get('/')
- self.assertRaises(AssertionError, view, request)
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_unknown_filter(self):
- """
- GET requests with filters that aren't configured should return 200.
- """
- view = FilterFieldsRootView.as_view()
-
- search_integer = 10
- request = factory.get('/?integer=%s' % search_integer)
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-class IntegrationTestDetailFiltering(CommonFilteringTestCase):
- """
- Integration tests for filtered detail views.
- """
- urls = 'rest_framework.tests.test_filters'
-
- def _get_url(self, item):
- return reverse('detail-view', kwargs=dict(pk=item.pk))
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_filtered_detail_view(self):
- """
- GET requests to filtered RetrieveAPIView that have a filter_class set
- should return filtered results.
- """
- item = self.objects.all()[0]
- data = self._serialize_object(item)
-
- # Basic test with no filter.
- response = self.client.get(self._get_url(item))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, data)
-
- # Tests that the decimal filter set that should fail.
- search_decimal = Decimal('4.25')
- high_item = self.objects.filter(decimal__gt=search_decimal)[0]
- response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal))
- self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
-
- # Tests that the decimal filter set that should succeed.
- search_decimal = Decimal('4.25')
- low_item = self.objects.filter(decimal__lt=search_decimal)[0]
- low_item_data = self._serialize_object(low_item)
- response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, low_item_data)
-
- # Tests that multiple filters works.
- search_decimal = Decimal('5.25')
- search_date = datetime.date(2012, 10, 2)
- valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
- valid_item_data = self._serialize_object(valid_item)
- response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date))
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, valid_item_data)
-
-
-class SearchFilterModel(models.Model):
- title = models.CharField(max_length=20)
- text = models.CharField(max_length=100)
-
-
-class SearchFilterTests(TestCase):
- def setUp(self):
- # Sequence of title/text is:
- #
- # z abc
- # zz bcd
- # zzz cde
- # ...
- for idx in range(10):
- title = 'z' * (idx + 1)
- text = (
- chr(idx + ord('a')) +
- chr(idx + ord('b')) +
- chr(idx + ord('c'))
- )
- SearchFilterModel(title=title, text=text).save()
-
- def test_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('title', 'text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=b')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 1, 'title': 'z', 'text': 'abc'},
- {'id': 2, 'title': 'zz', 'text': 'bcd'}
- ]
- )
-
- def test_exact_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('=title', 'text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=zzz')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'zzz', 'text': 'cde'}
- ]
- )
-
- def test_startswith_search(self):
- class SearchListView(generics.ListAPIView):
- model = SearchFilterModel
- filter_backends = (filters.SearchFilter,)
- search_fields = ('title', '^text')
-
- view = SearchListView.as_view()
- request = factory.get('?search=b')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 2, 'title': 'zz', 'text': 'bcd'}
- ]
- )
-
-
-class OrdringFilterModel(models.Model):
- title = models.CharField(max_length=20)
- text = models.CharField(max_length=100)
-
-
-class OrderingFilterTests(TestCase):
- def setUp(self):
- # Sequence of title/text is:
- #
- # zyx abc
- # yxw bcd
- # xwv cde
- for idx in range(3):
- title = (
- chr(ord('z') - idx) +
- chr(ord('y') - idx) +
- chr(ord('x') - idx)
- )
- text = (
- chr(idx + ord('a')) +
- chr(idx + ord('b')) +
- chr(idx + ord('c'))
- )
- OrdringFilterModel(title=title, text=text).save()
-
- def test_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=text')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- ]
- )
-
- def test_reverse_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=-text')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_incorrectfield_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('?ordering=foobar')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_default_ordering(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = ('title',)
-
- view = OrderingListView.as_view()
- request = factory.get('')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
-
- def test_default_ordering_using_string(self):
- class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
- filter_backends = (filters.OrderingFilter,)
- ordering = 'title'
-
- view = OrderingListView.as_view()
- request = factory.get('')
- response = view(request)
- self.assertEqual(
- response.data,
- [
- {'id': 3, 'title': 'xwv', 'text': 'cde'},
- {'id': 2, 'title': 'yxw', 'text': 'bcd'},
- {'id': 1, 'title': 'zyx', 'text': 'abc'},
- ]
- )
diff --git a/rest_framework/tests/test_hyperlinkedserializers.py b/rest_framework/tests/test_hyperlinkedserializers.py
deleted file mode 100644
index 61e613d7..00000000
--- a/rest_framework/tests/test_hyperlinkedserializers.py
+++ /dev/null
@@ -1,333 +0,0 @@
-from __future__ import unicode_literals
-import json
-from django.test import TestCase
-from rest_framework import generics, status, serializers
-from rest_framework.compat import patterns, url
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import (
- Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment,
- Album, Photo, OptionalRelationModel
-)
-
-factory = APIRequestFactory()
-
-
-class BlogPostCommentSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='blogpostcomment-detail')
- text = serializers.CharField()
- blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail')
-
- class Meta:
- model = BlogPostComment
- fields = ('text', 'blog_post_url', 'url')
-
-
-class PhotoSerializer(serializers.Serializer):
- description = serializers.CharField()
- album_url = serializers.HyperlinkedRelatedField(source='album', view_name='album-detail', queryset=Album.objects.all(), lookup_field='title', slug_url_kwarg='title')
-
- def restore_object(self, attrs, instance=None):
- return Photo(**attrs)
-
-
-class AlbumSerializer(serializers.ModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
-
- class Meta:
- model = Album
- fields = ('title', 'url')
-
-
-class BasicList(generics.ListCreateAPIView):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class BasicDetail(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class AnchorDetail(generics.RetrieveAPIView):
- model = Anchor
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class ManyToManyList(generics.ListAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class ManyToManyDetail(generics.RetrieveAPIView):
- model = ManyToManyModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-class BlogPostCommentListCreate(generics.ListCreateAPIView):
- model = BlogPostComment
- serializer_class = BlogPostCommentSerializer
-
-
-class BlogPostCommentDetail(generics.RetrieveAPIView):
- model = BlogPostComment
- serializer_class = BlogPostCommentSerializer
-
-
-class BlogPostDetail(generics.RetrieveAPIView):
- model = BlogPost
-
-
-class PhotoListCreate(generics.ListCreateAPIView):
- model = Photo
- model_serializer_class = PhotoSerializer
-
-
-class AlbumDetail(generics.RetrieveAPIView):
- model = Album
- serializer_class = AlbumSerializer
- lookup_field = 'title'
-
-
-class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
- model = OptionalRelationModel
- model_serializer_class = serializers.HyperlinkedModelSerializer
-
-
-urlpatterns = patterns('',
- url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
- url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
- url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
- url(r'^manytomany/$', ManyToManyList.as_view(), name='manytomanymodel-list'),
- url(r'^manytomany/(?P<pk>\d+)/$', ManyToManyDetail.as_view(), name='manytomanymodel-detail'),
- url(r'^posts/(?P<pk>\d+)/$', BlogPostDetail.as_view(), name='blogpost-detail'),
- url(r'^comments/$', BlogPostCommentListCreate.as_view(), name='blogpostcomment-list'),
- url(r'^comments/(?P<pk>\d+)/$', BlogPostCommentDetail.as_view(), name='blogpostcomment-detail'),
- url(r'^albums/(?P<title>\w[\w-]*)/$', AlbumDetail.as_view(), name='album-detail'),
- url(r'^photos/$', PhotoListCreate.as_view(), name='photo-list'),
- url(r'^optionalrelation/(?P<pk>\d+)/$', OptionalRelationDetail.as_view(), name='optionalrelationmodel-detail'),
-)
-
-
-class TestBasicHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- for item in items:
- BasicModel(text=item).save()
- self.objects = BasicModel.objects
- self.data = [
- {'url': 'http://testserver/basic/%d/' % obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.list_view = BasicList.as_view()
- self.detail_view = BasicDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/')
- response = self.list_view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/basic/1')
- response = self.detail_view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestManyToManyHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 BasicModel instances.
- """
- items = ['foo', 'bar', 'baz']
- anchors = []
- for item in items:
- anchor = Anchor(text=item)
- anchor.save()
- anchors.append(anchor)
-
- manytomany = ManyToManyModel()
- manytomany.save()
- manytomany.rel.add(*anchors)
-
- self.data = [{
- 'url': 'http://testserver/manytomany/1/',
- 'rel': [
- 'http://testserver/anchor/1/',
- 'http://testserver/anchor/2/',
- 'http://testserver/anchor/3/',
- ]
- }]
- self.list_view = ManyToManyList.as_view()
- self.detail_view = ManyToManyDetail.as_view()
-
- def test_get_list_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/')
- response = self.list_view(request)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_get_detail_view(self):
- """
- GET requests to ListCreateAPIView should return list of objects.
- """
- request = factory.get('/manytomany/1/')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[0])
-
-
-class TestHyperlinkedIdentityFieldLookup(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 3 Album instances.
- """
- titles = ['foo', 'bar', 'baz']
- for title in titles:
- album = Album(title=title)
- album.save()
- self.detail_view = AlbumDetail.as_view()
- self.data = {
- 'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
- 'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
- 'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
- }
-
- def test_lookup_field(self):
- """
- GET requests to AlbumDetail view should return serialized Albums
- with a url field keyed by `title`.
- """
- for album in Album.objects.all():
- request = factory.get('/albums/{0}/'.format(album.title))
- response = self.detail_view(request, title=album.title)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data[album.title])
-
-
-class TestCreateWithForeignKeys(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create a blog post
- """
- self.post = BlogPost.objects.create(title="Test post")
- self.create_view = BlogPostCommentListCreate.as_view()
-
- def test_create_comment(self):
-
- data = {
- 'text': 'A test comment',
- 'blog_post_url': 'http://testserver/posts/1/'
- }
-
- request = factory.post('/comments/', data=data)
- response = self.create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response['Location'], 'http://testserver/comments/1/')
- self.assertEqual(self.post.blogpostcomment_set.count(), 1)
- self.assertEqual(self.post.blogpostcomment_set.all()[0].text, 'A test comment')
-
-
-class TestCreateWithForeignKeysAndCustomSlug(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create an Album
- """
- self.post = Album.objects.create(title='test-album')
- self.list_create_view = PhotoListCreate.as_view()
-
- def test_create_photo(self):
-
- data = {
- 'description': 'A test photo',
- 'album_url': 'http://testserver/albums/test-album/'
- }
-
- request = factory.post('/photos/', data=data)
- response = self.list_create_view(request)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertNotIn('Location', response, msg='Location should only be included if there is a "url" field on the serializer')
- self.assertEqual(self.post.photo_set.count(), 1)
- self.assertEqual(self.post.photo_set.all()[0].description, 'A test photo')
-
-
-class TestOptionalRelationHyperlinkedView(TestCase):
- urls = 'rest_framework.tests.test_hyperlinkedserializers'
-
- def setUp(self):
- """
- Create 1 OptionalRelationModel instances.
- """
- OptionalRelationModel().save()
- self.objects = OptionalRelationModel.objects
- self.detail_view = OptionalRelationDetail.as_view()
- self.data = {"url": "http://testserver/optionalrelation/1/", "other": None}
-
- def test_get_detail_view(self):
- """
- GET requests to RetrieveAPIView with optional relations should return None
- for non existing relations.
- """
- request = factory.get('/optionalrelationmodel-detail/1')
- response = self.detail_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, self.data)
-
- def test_put_detail_view(self):
- """
- PUT requests to RetrieveUpdateDestroyAPIView with optional relations
- should accept None for non existing relations.
- """
- response = self.client.put('/optionalrelation/1/',
- data=json.dumps(self.data),
- content_type='application/json')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-class TestOverriddenURLField(TestCase):
- def setUp(self):
- class OverriddenURLSerializer(serializers.HyperlinkedModelSerializer):
- url = serializers.SerializerMethodField('get_url')
-
- class Meta:
- model = BlogPost
- fields = ('title', 'url')
-
- def get_url(self, obj):
- return 'foo bar'
-
- self.Serializer = OverriddenURLSerializer
- self.obj = BlogPost.objects.create(title='New blog post')
-
- def test_overridden_url_field(self):
- """
- The 'url' field should respect overriding.
- Regression test for #936.
- """
- serializer = self.Serializer(self.obj)
- self.assertEqual(
- serializer.data,
- {'title': 'New blog post', 'url': 'foo bar'}
- )
diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py
deleted file mode 100644
index 85d4640e..00000000
--- a/rest_framework/tests/test_pagination.py
+++ /dev/null
@@ -1,385 +0,0 @@
-from __future__ import unicode_literals
-import datetime
-from decimal import Decimal
-from django.db import models
-from django.core.paginator import Paginator
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework import generics, status, pagination, filters, serializers
-from rest_framework.compat import django_filters
-from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel
-
-factory = APIRequestFactory()
-
-
-class FilterableItem(models.Model):
- text = models.CharField(max_length=100)
- decimal = models.DecimalField(max_digits=4, decimal_places=2)
- date = models.DateField()
-
-
-class RootView(generics.ListCreateAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
- paginate_by = 10
-
-
-class DefaultPageSizeKwargView(generics.ListAPIView):
- """
- View for testing default paginate_by_param usage
- """
- model = BasicModel
-
-
-class PaginateByParamView(generics.ListAPIView):
- """
- View for testing custom paginate_by_param usage
- """
- model = BasicModel
- paginate_by_param = 'page_size'
-
-
-class IntegrationTestPagination(TestCase):
- """
- Integration tests for paginated list views.
- """
-
- def setUp(self):
- """
- Create 26 BasicModel instances.
- """
- for char in 'abcdefghijklmnopqrstuvwxyz':
- BasicModel(text=char * 3).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = RootView.as_view()
-
- def test_get_paginated_root_view(self):
- """
- GET requests to paginated ListCreateAPIView should return paginated results.
- """
- request = factory.get('/')
- # Note: Database queries are a `SELECT COUNT`, and `SELECT <fields>`
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[10:20])
- self.assertNotEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = self.view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 26)
- self.assertEqual(response.data['results'], self.data[20:])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
-
-class IntegrationTestPaginationAndFiltering(TestCase):
-
- def setUp(self):
- """
- Create 50 FilterableItem instances.
- """
- base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
- for i in range(26):
- text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
- decimal = base_data[1] + i
- date = base_data[2] - datetime.timedelta(days=i * 2)
- FilterableItem(text=text, decimal=decimal, date=date).save()
-
- self.objects = FilterableItem.objects
- self.data = [
- {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date}
- for obj in self.objects.all()
- ]
-
- @unittest.skipUnless(django_filters, 'django-filters not installed')
- def test_get_django_filter_paginated_filtered_root_view(self):
- """
- GET requests to paginated filtered ListCreateAPIView should return
- paginated results. The next and previous links should preserve the
- filtered parameters.
- """
- class DecimalFilter(django_filters.FilterSet):
- decimal = django_filters.NumberFilter(lookup_type='lt')
-
- class Meta:
- model = FilterableItem
- fields = ['text', 'decimal', 'date']
-
- class FilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- paginate_by = 10
- filter_class = DecimalFilter
- filter_backends = (filters.DjangoFilterBackend,)
-
- view = FilterFieldsRootView.as_view()
-
- EXPECTED_NUM_QUERIES = 2
-
- request = factory.get('/?decimal=15.20')
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[10:15])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['previous'])
- with self.assertNumQueries(EXPECTED_NUM_QUERIES):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- def test_get_basic_paginated_filtered_root_view(self):
- """
- Same as `test_get_django_filter_paginated_filtered_root_view`,
- except using a custom filter backend instead of the django-filter
- backend,
- """
-
- class DecimalFilterBackend(filters.BaseFilterBackend):
- def filter_queryset(self, request, queryset, view):
- return queryset.filter(decimal__lt=Decimal(request.GET['decimal']))
-
- class BasicFilterFieldsRootView(generics.ListCreateAPIView):
- model = FilterableItem
- paginate_by = 10
- filter_backends = (DecimalFilterBackend,)
-
- view = BasicFilterFieldsRootView.as_view()
-
- request = factory.get('/?decimal=15.20')
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
- request = factory.get(response.data['next'])
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[10:15])
- self.assertEqual(response.data['next'], None)
- self.assertNotEqual(response.data['previous'], None)
-
- request = factory.get(response.data['previous'])
- with self.assertNumQueries(2):
- response = view(request).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data['count'], 15)
- self.assertEqual(response.data['results'], self.data[:10])
- self.assertNotEqual(response.data['next'], None)
- self.assertEqual(response.data['previous'], None)
-
-
-class PassOnContextPaginationSerializer(pagination.PaginationSerializer):
- class Meta:
- object_serializer_class = serializers.Serializer
-
-
-class UnitTestPagination(TestCase):
- """
- Unit tests for pagination of primitive objects.
- """
-
- def setUp(self):
- self.objects = [char * 3 for char in 'abcdefghijklmnopqrstuvwxyz']
- paginator = Paginator(self.objects, 10)
- self.first_page = paginator.page(1)
- self.last_page = paginator.page(3)
-
- def test_native_pagination(self):
- serializer = pagination.PaginationSerializer(self.first_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], '?page=2')
- self.assertEqual(serializer.data['previous'], None)
- self.assertEqual(serializer.data['results'], self.objects[:10])
-
- serializer = pagination.PaginationSerializer(self.last_page)
- self.assertEqual(serializer.data['count'], 26)
- self.assertEqual(serializer.data['next'], None)
- self.assertEqual(serializer.data['previous'], '?page=2')
- self.assertEqual(serializer.data['results'], self.objects[20:])
-
- def test_context_available_in_result(self):
- """
- Ensure context gets passed through to the object serializer.
- """
- serializer = PassOnContextPaginationSerializer(self.first_page, context={'foo': 'bar'})
- serializer.data
- results = serializer.fields[serializer.results_field]
- self.assertEqual(serializer.context, results.context)
-
-
-class TestUnpaginated(TestCase):
- """
- Tests for list views without pagination.
- """
-
- def setUp(self):
- """
- Create 13 BasicModel instances.
- """
- for i in range(13):
- BasicModel(text=i).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = DefaultPageSizeKwargView.as_view()
-
- def test_unpaginated(self):
- """
- Tests the default page size for this view.
- no page size --> no limit --> no meta data
- """
- request = factory.get('/')
- response = self.view(request)
- self.assertEqual(response.data, self.data)
-
-
-class TestCustomPaginateByParam(TestCase):
- """
- Tests for list views with default page size kwarg
- """
-
- def setUp(self):
- """
- Create 13 BasicModel instances.
- """
- for i in range(13):
- BasicModel(text=i).save()
- self.objects = BasicModel.objects
- self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
- ]
- self.view = PaginateByParamView.as_view()
-
- def test_default_page_size(self):
- """
- Tests the default page size for this view.
- no page size --> no limit --> no meta data
- """
- request = factory.get('/')
- response = self.view(request).render()
- self.assertEqual(response.data, self.data)
-
- def test_paginate_by_param(self):
- """
- If paginate_by_param is set, the new kwarg should limit per view requests.
- """
- request = factory.get('/?page_size=5')
- response = self.view(request).render()
- self.assertEqual(response.data['count'], 13)
- self.assertEqual(response.data['results'], self.data[:5])
-
-
-### Tests for context in pagination serializers
-
-class CustomField(serializers.Field):
- def to_native(self, value):
- if not 'view' in self.context:
- raise RuntimeError("context isn't getting passed into custom field")
- return "value"
-
-
-class BasicModelSerializer(serializers.Serializer):
- text = CustomField()
-
- def __init__(self, *args, **kwargs):
- super(BasicModelSerializer, self).__init__(*args, **kwargs)
- if not 'view' in self.context:
- raise RuntimeError("context isn't getting passed into serializer init")
-
-
-class TestContextPassedToCustomField(TestCase):
- def setUp(self):
- BasicModel.objects.create(text='ala ma kota')
-
- def test_with_pagination(self):
- class ListView(generics.ListCreateAPIView):
- model = BasicModel
- serializer_class = BasicModelSerializer
- paginate_by = 1
-
- self.view = ListView.as_view()
- request = factory.get('/')
- response = self.view(request).render()
-
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-### Tests for custom pagination serializers
-
-class LinksSerializer(serializers.Serializer):
- next = pagination.NextPageField(source='*')
- prev = pagination.PreviousPageField(source='*')
-
-
-class CustomPaginationSerializer(pagination.BasePaginationSerializer):
- links = LinksSerializer(source='*') # Takes the page object as the source
- total_results = serializers.Field(source='paginator.count')
-
- results_field = 'objects'
-
-
-class TestCustomPaginationSerializer(TestCase):
- def setUp(self):
- objects = ['john', 'paul', 'george', 'ringo']
- paginator = Paginator(objects, 2)
- self.page = paginator.page(1)
-
- def test_custom_pagination_serializer(self):
- request = APIRequestFactory().get('/foobar')
- serializer = CustomPaginationSerializer(
- instance=self.page,
- context={'request': request}
- )
- expected = {
- 'links': {
- 'next': 'http://testserver/foobar?page=2',
- 'prev': None
- },
- 'total_results': 4,
- 'objects': ['john', 'paul']
- }
- self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_parsers.py b/rest_framework/tests/test_parsers.py
deleted file mode 100644
index 7699e10c..00000000
--- a/rest_framework/tests/test_parsers.py
+++ /dev/null
@@ -1,115 +0,0 @@
-from __future__ import unicode_literals
-from rest_framework.compat import StringIO
-from django import forms
-from django.core.files.uploadhandler import MemoryFileUploadHandler
-from django.test import TestCase
-from django.utils import unittest
-from rest_framework.compat import etree
-from rest_framework.parsers import FormParser, FileUploadParser
-from rest_framework.parsers import XMLParser
-import datetime
-
-
-class Form(forms.Form):
- field1 = forms.CharField(max_length=3)
- field2 = forms.CharField()
-
-
-class TestFormParser(TestCase):
- def setUp(self):
- self.string = "field1=abc&field2=defghijk"
-
- def test_parse(self):
- """ Make sure the `QueryDict` works OK """
- parser = FormParser()
-
- stream = StringIO(self.string)
- data = parser.parse(stream)
-
- self.assertEqual(Form(data).is_valid(), True)
-
-
-class TestXMLParser(TestCase):
- def setUp(self):
- self._input = StringIO(
- '<?xml version="1.0" encoding="utf-8"?>'
- '<root>'
- '<field_a>121.0</field_a>'
- '<field_b>dasd</field_b>'
- '<field_c></field_c>'
- '<field_d>2011-12-25 12:45:00</field_d>'
- '</root>'
- )
- self._data = {
- 'field_a': 121,
- 'field_b': 'dasd',
- 'field_c': None,
- 'field_d': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }
- self._complex_data_input = StringIO(
- '<?xml version="1.0" encoding="utf-8"?>'
- '<root>'
- '<creation_date>2011-12-25 12:45:00</creation_date>'
- '<sub_data_list>'
- '<list-item><sub_id>1</sub_id><sub_name>first</sub_name></list-item>'
- '<list-item><sub_id>2</sub_id><sub_name>second</sub_name></list-item>'
- '</sub_data_list>'
- '<name>name</name>'
- '</root>'
- )
- self._complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_parse(self):
- parser = XMLParser()
- data = parser.parse(self._input)
- self.assertEqual(data, self._data)
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_complex_data_parse(self):
- parser = XMLParser()
- data = parser.parse(self._complex_data_input)
- self.assertEqual(data, self._complex_data)
-
-
-class TestFileUploadParser(TestCase):
- def setUp(self):
- class MockRequest(object):
- pass
- from io import BytesIO
- self.stream = BytesIO(
- "Test text file".encode('utf-8')
- )
- request = MockRequest()
- request.upload_handlers = (MemoryFileUploadHandler(),)
- request.META = {
- 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'),
- 'HTTP_CONTENT_LENGTH': 14,
- }
- self.parser_context = {'request': request, 'kwargs': {}}
-
- def test_parse(self):
- """ Make sure the `QueryDict` works OK """
- parser = FileUploadParser()
- self.stream.seek(0)
- data_and_files = parser.parse(self.stream, None, self.parser_context)
- file_obj = data_and_files.files['file']
- self.assertEqual(file_obj._size, 14)
-
- def test_get_filename(self):
- parser = FileUploadParser()
- filename = parser.get_filename(self.stream, None, self.parser_context)
- self.assertEqual(filename, 'file.txt'.encode('utf-8'))
diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py
deleted file mode 100644
index e2cca380..00000000
--- a/rest_framework/tests/test_permissions.py
+++ /dev/null
@@ -1,188 +0,0 @@
-from __future__ import unicode_literals
-from django.contrib.auth.models import User, Permission
-from django.db import models
-from django.test import TestCase
-from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING
-from rest_framework.test import APIRequestFactory
-import base64
-
-factory = APIRequestFactory()
-
-
-class BasicModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class RootView(generics.ListCreateAPIView):
- model = BasicModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [permissions.DjangoModelPermissions]
-
-
-class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = BasicModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [permissions.DjangoModelPermissions]
-
-root_view = RootView.as_view()
-instance_view = InstanceView.as_view()
-
-
-def basic_auth_header(username, password):
- credentials = ('%s:%s' % (username, password))
- base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
- return 'Basic %s' % base64_credentials
-
-
-class ModelPermissionsIntegrationTests(TestCase):
- def setUp(self):
- User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
- user = User.objects.create_user('permitted', 'permitted@example.com', 'password')
- user.user_permissions = [
- Permission.objects.get(codename='add_basicmodel'),
- Permission.objects.get(codename='change_basicmodel'),
- Permission.objects.get(codename='delete_basicmodel')
- ]
- user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
- user.user_permissions = [
- Permission.objects.get(codename='change_basicmodel'),
- ]
-
- self.permitted_credentials = basic_auth_header('permitted', 'password')
- self.disallowed_credentials = basic_auth_header('disallowed', 'password')
- self.updateonly_credentials = basic_auth_header('updateonly', 'password')
-
- BasicModel(text='foo').save()
-
- def test_has_create_permissions(self):
- request = factory.post('/', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = root_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-
- def test_has_put_permissions(self):
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- def test_has_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- def test_does_not_have_create_permissions(self):
- request = factory.post('/', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = root_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_does_not_have_put_permissions(self):
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_does_not_have_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk=1)
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_has_put_as_create_permissions(self):
- # User only has update permissions - should be able to update an entity.
- request = factory.put('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
- # But if PUTing to a new entity, permission should be denied.
- request = factory.put('/2', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='2')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
- def test_options_permitted(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['POST'])
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.permitted_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
-
- def test_options_disallowed(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- def test_options_updateonly(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = root_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertNotIn('actions', response.data)
-
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
- response = instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertIn('actions', response.data)
- self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
-
-
-class OwnerModel(models.Model):
- text = models.CharField(max_length=100)
- owner = models.ForeignKey(User)
-
-
-class IsOwnerPermission(permissions.BasePermission):
- def has_object_permission(self, request, view, obj):
- return request.user == obj.owner
-
-
-class OwnerInstanceView(generics.RetrieveUpdateDestroyAPIView):
- model = OwnerModel
- authentication_classes = [authentication.BasicAuthentication]
- permission_classes = [IsOwnerPermission]
-
-
-owner_instance_view = OwnerInstanceView.as_view()
-
-
-class ObjectPermissionsIntegrationTests(TestCase):
- """
- Integration tests for the object level permissions API.
- """
-
- def setUp(self):
- User.objects.create_user('not_owner', 'not_owner@example.com', 'password')
- user = User.objects.create_user('owner', 'owner@example.com', 'password')
-
- self.not_owner_credentials = basic_auth_header('not_owner', 'password')
- self.owner_credentials = basic_auth_header('owner', 'password')
-
- OwnerModel(text='foo', owner=user).save()
-
- def test_owner_has_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.owner_credentials)
- response = owner_instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
-
- def test_non_owner_does_not_have_delete_permissions(self):
- request = factory.delete('/1', HTTP_AUTHORIZATION=self.not_owner_credentials)
- response = owner_instance_view(request, pk='1')
- self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py
deleted file mode 100644
index d19219c9..00000000
--- a/rest_framework/tests/test_relations.py
+++ /dev/null
@@ -1,100 +0,0 @@
-"""
-General tests for relational fields.
-"""
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.tests.models import BlogPost
-
-
-class NullModel(models.Model):
- pass
-
-
-class FieldTests(TestCase):
- def test_pk_related_field_with_empty_string(self):
- """
- Regression test for #446
-
- https://github.com/tomchristie/django-rest-framework/issues/446
- """
- field = serializers.PrimaryKeyRelatedField(queryset=NullModel.objects.all())
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_hyperlinked_related_field_with_empty_string(self):
- field = serializers.HyperlinkedRelatedField(queryset=NullModel.objects.all(), view_name='')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
- def test_slug_related_field_with_empty_string(self):
- field = serializers.SlugRelatedField(queryset=NullModel.objects.all(), slug_field='pk')
- self.assertRaises(serializers.ValidationError, field.from_native, '')
- self.assertRaises(serializers.ValidationError, field.from_native, [])
-
-
-class TestManyRelatedMixin(TestCase):
- def test_missing_many_to_many_related_field(self):
- '''
- Regression test for #632
-
- https://github.com/tomchristie/django-rest-framework/pull/632
- '''
- field = serializers.RelatedField(many=True, read_only=False)
-
- into = {}
- field.field_from_native({}, None, 'field_name', into)
- self.assertEqual(into['field_name'], [])
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class RelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.RelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['BlogPost object'])
diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py
deleted file mode 100644
index f6d006b3..00000000
--- a/rest_framework/tests/test_relations_nested.py
+++ /dev/null
@@ -1,107 +0,0 @@
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-from rest_framework.tests.models import ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
-
-
-class ForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeySource
- fields = ('id', 'name', 'target')
- depth = 1
-
-
-class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ForeignKeyTarget
- fields = ('id', 'name', 'sources')
- depth = 1
-
-
-class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = NullableForeignKeySource
- fields = ('id', 'name', 'target')
- depth = 1
-
-
-class NullableOneToOneTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = OneToOneTarget
- fields = ('id', 'name', 'nullable_source')
- depth = 1
-
-
-class ReverseForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- new_target = ForeignKeyTarget(name='target-2')
- new_target.save()
- for idx in range(1, 4):
- source = ForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve(self):
- queryset = ForeignKeySource.objects.all()
- serializer = ForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 3, 'name': 'source-3', 'target': {'id': 1, 'name': 'target-1'}},
- ]
- self.assertEqual(serializer.data, expected)
-
- def test_reverse_foreign_key_retrieve(self):
- queryset = ForeignKeyTarget.objects.all()
- serializer = ForeignKeyTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'sources': [
- {'id': 1, 'name': 'source-1', 'target': 1},
- {'id': 2, 'name': 'source-2', 'target': 1},
- {'id': 3, 'name': 'source-3', 'target': 1},
- ]},
- {'id': 2, 'name': 'target-2', 'sources': [
- ]}
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class NestedNullableForeignKeyTests(TestCase):
- def setUp(self):
- target = ForeignKeyTarget(name='target-1')
- target.save()
- for idx in range(1, 4):
- if idx == 3:
- target = None
- source = NullableForeignKeySource(name='source-%d' % idx, target=target)
- source.save()
-
- def test_foreign_key_retrieve_with_null(self):
- queryset = NullableForeignKeySource.objects.all()
- serializer = NullableForeignKeySourceSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'source-1', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 2, 'name': 'source-2', 'target': {'id': 1, 'name': 'target-1'}},
- {'id': 3, 'name': 'source-3', 'target': None},
- ]
- self.assertEqual(serializer.data, expected)
-
-
-class NestedNullableOneToOneTests(TestCase):
- def setUp(self):
- target = OneToOneTarget(name='target-1')
- target.save()
- new_target = OneToOneTarget(name='target-2')
- new_target.save()
- source = NullableOneToOneSource(name='source-1', target=target)
- source.save()
-
- def test_reverse_foreign_key_retrieve_with_null(self):
- queryset = OneToOneTarget.objects.all()
- serializer = NullableOneToOneTargetSerializer(queryset, many=True)
- expected = [
- {'id': 1, 'name': 'target-1', 'nullable_source': {'id': 1, 'name': 'source-1', 'target': 1}},
- {'id': 2, 'name': 'target-2', 'nullable_source': None},
- ]
- self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_routers.py b/rest_framework/tests/test_routers.py
deleted file mode 100644
index 5fcccb74..00000000
--- a/rest_framework/tests/test_routers.py
+++ /dev/null
@@ -1,216 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from django.core.exceptions import ImproperlyConfigured
-from rest_framework import serializers, viewsets, permissions
-from rest_framework.compat import include, patterns, url
-from rest_framework.decorators import link, action
-from rest_framework.response import Response
-from rest_framework.routers import SimpleRouter, DefaultRouter
-from rest_framework.test import APIRequestFactory
-
-factory = APIRequestFactory()
-
-urlpatterns = patterns('',)
-
-
-class BasicViewSet(viewsets.ViewSet):
- def list(self, request, *args, **kwargs):
- return Response({'method': 'list'})
-
- @action()
- def action1(self, request, *args, **kwargs):
- return Response({'method': 'action1'})
-
- @action()
- def action2(self, request, *args, **kwargs):
- return Response({'method': 'action2'})
-
- @action(methods=['post', 'delete'])
- def action3(self, request, *args, **kwargs):
- return Response({'method': 'action2'})
-
- @link()
- def link1(self, request, *args, **kwargs):
- return Response({'method': 'link1'})
-
- @link()
- def link2(self, request, *args, **kwargs):
- return Response({'method': 'link2'})
-
-
-class TestSimpleRouter(TestCase):
- def setUp(self):
- self.router = SimpleRouter()
-
- def test_link_and_action_decorator(self):
- routes = self.router.get_routes(BasicViewSet)
- decorator_routes = routes[2:]
- # Make sure all these endpoints exist and none have been clobbered
- for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
- route = decorator_routes[i]
- # check url listing
- self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
- # check method to function mapping
- if endpoint == 'action3':
- methods_map = ['post', 'delete']
- elif endpoint.startswith('action'):
- methods_map = ['post']
- else:
- methods_map = ['get']
- for method in methods_map:
- self.assertEqual(route.mapping[method], endpoint)
-
-
-class RouterTestModel(models.Model):
- uuid = models.CharField(max_length=20)
- text = models.CharField(max_length=200)
-
-
-class TestCustomLookupFields(TestCase):
- """
- Ensure that custom lookup fields are correctly routed.
- """
- urls = 'rest_framework.tests.test_routers'
-
- def setUp(self):
- class NoteSerializer(serializers.HyperlinkedModelSerializer):
- class Meta:
- model = RouterTestModel
- lookup_field = 'uuid'
- fields = ('url', 'uuid', 'text')
-
- class NoteViewSet(viewsets.ModelViewSet):
- queryset = RouterTestModel.objects.all()
- serializer_class = NoteSerializer
- lookup_field = 'uuid'
-
- RouterTestModel.objects.create(uuid='123', text='foo bar')
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
-
- from rest_framework.tests import test_routers
- urls = getattr(test_routers, 'urlpatterns')
- urls += patterns('',
- url(r'^', include(self.router.urls)),
- )
-
- def test_custom_lookup_field_route(self):
- detail_route = self.router.urls[-1]
- detail_url_pattern = detail_route.regex.pattern
- self.assertIn('<uuid>', detail_url_pattern)
-
- def test_retrieve_lookup_field_list_view(self):
- response = self.client.get('/notes/')
- self.assertEqual(response.data,
- [{
- "url": "http://testserver/notes/123/",
- "uuid": "123", "text": "foo bar"
- }]
- )
-
- def test_retrieve_lookup_field_detail_view(self):
- response = self.client.get('/notes/123/')
- self.assertEqual(response.data,
- {
- "url": "http://testserver/notes/123/",
- "uuid": "123", "text": "foo bar"
- }
- )
-
-
-class TestTrailingSlashIncluded(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_urls_have_trailing_slash_by_default(self):
- expected = ['^notes/$', '^notes/(?P<pk>[^/]+)/$']
- for idx in range(len(expected)):
- self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
-
-
-class TestTrailingSlashRemoved(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
-
- self.router = SimpleRouter(trailing_slash=False)
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_urls_can_have_trailing_slash_removed(self):
- expected = ['^notes$', '^notes/(?P<pk>[^/]+)$']
- for idx in range(len(expected)):
- self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
-
-
-class TestNameableRoot(TestCase):
- def setUp(self):
- class NoteViewSet(viewsets.ModelViewSet):
- model = RouterTestModel
- self.router = DefaultRouter()
- self.router.root_view_name = 'nameable-root'
- self.router.register(r'notes', NoteViewSet)
- self.urls = self.router.urls
-
- def test_router_has_custom_name(self):
- expected = 'nameable-root'
- self.assertEqual(expected, self.urls[0].name)
-
-
-class TestActionKeywordArgs(TestCase):
- """
- Ensure keyword arguments passed in the `@action` decorator
- are properly handled. Refs #940.
- """
-
- def setUp(self):
- class TestViewSet(viewsets.ModelViewSet):
- permission_classes = []
-
- @action(permission_classes=[permissions.AllowAny])
- def custom(self, request, *args, **kwargs):
- return Response({
- 'permission_classes': self.permission_classes
- })
-
- self.router = SimpleRouter()
- self.router.register(r'test', TestViewSet, base_name='test')
- self.view = self.router.urls[-1].callback
-
- def test_action_kwargs(self):
- request = factory.post('/test/0/custom/')
- response = self.view(request)
- self.assertEqual(
- response.data,
- {'permission_classes': [permissions.AllowAny]}
- )
-
-
-class TestActionAppliedToExistingRoute(TestCase):
- """
- Ensure `@action` decorator raises an except when applied
- to an existing route
- """
-
- def test_exception_raised_when_action_applied_to_existing_route(self):
- class TestViewSet(viewsets.ModelViewSet):
-
- @action()
- def retrieve(self, request, *args, **kwargs):
- return Response({
- 'hello': 'world'
- })
-
- self.router = SimpleRouter()
- self.router.register(r'test', TestViewSet, base_name='test')
-
- with self.assertRaises(ImproperlyConfigured):
- self.router.urls
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
deleted file mode 100644
index c2497660..00000000
--- a/rest_framework/tests/test_serializer.py
+++ /dev/null
@@ -1,1645 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.db.models.fields import BLANK_CHOICE_DASH
-from django.test import TestCase
-from django.utils.datastructures import MultiValueDict
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers, fields, relations
-from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
- BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
-from rest_framework.tests.models import BasicModelSerializer
-import datetime
-import pickle
-
-
-class SubComment(object):
- def __init__(self, sub_comment):
- self.sub_comment = sub_comment
-
-
-class Comment(object):
- def __init__(self, email, content, created):
- self.email = email
- self.content = content
- self.created = created or datetime.datetime.now()
-
- def __eq__(self, other):
- return all([getattr(self, attr) == getattr(other, attr)
- for attr in ('email', 'content', 'created')])
-
- def get_sub_comment(self):
- sub_comment = SubComment('And Merry Christmas!')
- return sub_comment
-
-
-class CommentSerializer(serializers.Serializer):
- email = serializers.EmailField()
- content = serializers.CharField(max_length=1000)
- created = serializers.DateTimeField()
- sub_comment = serializers.Field(source='get_sub_comment.sub_comment')
-
- def restore_object(self, data, instance=None):
- if instance is None:
- return Comment(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
-
-
-class NamesSerializer(serializers.Serializer):
- first = serializers.CharField()
- last = serializers.CharField(required=False, default='')
- initials = serializers.CharField(required=False, default='')
-
-
-class PersonIdentifierSerializer(serializers.Serializer):
- ssn = serializers.CharField()
- names = NamesSerializer(source='names', required=False)
-
-
-class BookSerializer(serializers.ModelSerializer):
- isbn = serializers.RegexField(regex=r'^[0-9]{13}$', error_messages={'invalid': 'isbn has to be exact 13 numbers'})
-
- class Meta:
- model = Book
-
-
-class ActionItemSerializer(serializers.ModelSerializer):
-
- class Meta:
- model = ActionItem
-
-
-class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
-
- class Meta:
- model = ActionItem
-
- def restore_object(self, data, instance=None):
- if instance is None:
- return ActionItem(**data)
- for key, val in data.items():
- setattr(instance, key, val)
- return instance
-
-
-class PersonSerializer(serializers.ModelSerializer):
- info = serializers.Field(source='info')
-
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age',)
-
-
-class NestedSerializer(serializers.Serializer):
- info = serializers.Field()
-
-
-class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
- nested = NestedSerializer(source='*')
-
- class Meta:
- model = Person
-
-
-class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
- """
- Testing for #652.
- """
- info = serializers.Field(source='info')
-
- class Meta:
- model = Person
- fields = ('name', 'age', 'info')
- read_only_fields = ('age', 'info')
-
-
-class AlbumsSerializer(serializers.ModelSerializer):
-
- class Meta:
- model = Album
- fields = ['title'] # lists are also valid options
-
-
-class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
- class Meta:
- model = HasPositiveIntegerAsChoice
- fields = ['some_integer']
-
-
-class BasicTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'This wont change'
- }
- self.expected = {
- 'email': 'tom@example.com',
- 'content': 'Happy new year!',
- 'created': datetime.datetime(2012, 1, 1),
- 'sub_comment': 'And Merry Christmas!'
- }
- self.person_data = {'name': 'dwight', 'age': 35}
- self.person = Person(**self.person_data)
- self.person.save()
-
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': '',
- 'content': '',
- 'created': None,
- 'sub_comment': ''
- }
- self.assertEqual(serializer.data, expected)
-
- def test_retrieve(self):
- serializer = CommentSerializer(self.comment)
- self.assertEqual(serializer.data, self.expected)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertFalse(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_create_nested(self):
- """Test a serializer with nested data."""
- names = {'first': 'John', 'last': 'Doe', 'initials': 'jd'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- self.assertEqual(serializer.data['names'], names)
-
- def test_create_partial_nested(self):
- """Test a serializer with nested data which has missing fields."""
- names = {'first': 'John'}
- data = {'ssn': '1234567890', 'names': names}
- serializer = PersonIdentifierSerializer(data=data)
-
- expected_names = {'first': 'John', 'last': '', 'initials': ''}
- data['names'] = expected_names
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is expected_names)
- self.assertEqual(serializer.data['names'], expected_names)
-
- def test_null_nested(self):
- """Test a serializer with a nonexistent nested field"""
- data = {'ssn': '1234567890'}
- serializer = PersonIdentifierSerializer(data=data)
-
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
- self.assertFalse(serializer.object is data)
- expected = {'ssn': '1234567890', 'names': None}
- self.assertEqual(serializer.data, expected)
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['sub_comment'], 'And Merry Christmas!')
-
- def test_partial_update(self):
- msg = 'Merry New Year!'
- partial_data = {'content': msg}
- serializer = CommentSerializer(self.comment, data=partial_data)
- self.assertEqual(serializer.is_valid(), False)
- serializer = CommentSerializer(self.comment, data=partial_data, partial=True)
- expected = self.comment
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected)
- self.assertTrue(serializer.object is expected)
- self.assertEqual(serializer.data['content'], msg)
-
- def test_model_fields_as_expected(self):
- """
- Make sure that the fields returned are the same as defined
- in the Meta data
- """
- serializer = PersonSerializer(self.person)
- self.assertEqual(set(serializer.data.keys()),
- set(['name', 'age', 'info']))
-
- def test_field_with_dictionary(self):
- """
- Make sure that dictionaries from fields are left intact
- """
- serializer = PersonSerializer(self.person)
- expected = self.person_data
- self.assertEqual(serializer.data['info'], expected)
-
- def test_read_only_fields(self):
- """
- Attempting to update fields set as read_only should have no effect.
- """
- serializer = PersonSerializer(self.person, data={'name': 'dwight', 'age': 99})
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(serializer.errors, {})
- # Assert age is unchanged (35)
- self.assertEqual(instance.age, self.person_data['age'])
-
- def test_invalid_read_only_fields(self):
- """
- Regression test for #652.
- """
- self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, [])
-
-
-class DictStyleSerializer(serializers.Serializer):
- """
- Note that we don't have any `restore_object` method, so the default
- case of simply returning a dict will apply.
- """
- email = serializers.EmailField()
-
-
-class DictStyleSerializerTests(TestCase):
- def test_dict_style_deserialize(self):
- """
- Ensure serializers can deserialize into a dict.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data=data)
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
-
- def test_dict_style_serialize(self):
- """
- Ensure serializers can serialize dict objects.
- """
- data = {'email': 'foo@example.com'}
- serializer = DictStyleSerializer(data)
- self.assertEqual(serializer.data, data)
-
-
-class ValidationTests(TestCase):
- def setUp(self):
- self.comment = Comment(
- 'tom@example.com',
- 'Happy new year!',
- datetime.datetime(2012, 1, 1)
- )
- self.data = {
- 'email': 'tom@example.com',
- 'content': 'x' * 1001,
- 'created': datetime.datetime(2012, 1, 1)
- }
- self.actionitem = ActionItem(title='Some to do item',)
-
- def test_create(self):
- serializer = CommentSerializer(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update(self):
- serializer = CommentSerializer(self.comment, data=self.data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'content': ['Ensure this value has at most 1000 characters (it has 1001).']})
-
- def test_update_missing_field(self):
- data = {
- 'content': 'xxx',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = CommentSerializer(self.comment, data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'email': ['This field is required.']})
-
- def test_missing_bool_with_default(self):
- """Make sure that a boolean value with a 'False' value is not
- mistaken for not having a default."""
- data = {
- 'title': 'Some action item',
- #No 'done' value.
- }
- serializer = ActionItemSerializer(self.actionitem, data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_cross_field_validation(self):
-
- class CommentSerializerWithCrossFieldValidator(CommentSerializer):
-
- def validate(self, attrs):
- if attrs["email"] not in attrs["content"]:
- raise serializers.ValidationError("Email address not in content")
- return attrs
-
- data = {
- 'email': 'tom@example.com',
- 'content': 'A comment from tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'A comment from foo@bar.com'
-
- serializer = CommentSerializerWithCrossFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'non_field_errors': ['Email address not in content']})
-
- def test_null_is_true_fields(self):
- """
- Omitting a value for null-field should validate.
- """
- serializer = PersonSerializer(data={'name': 'marko'})
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.errors, {})
-
- def test_modelserializer_max_length_exceeded(self):
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_modelserializer_max_length_exceeded_with_custom_restore(self):
- """
- When overriding ModelSerializer.restore_object, validation tests should still apply.
- Regression test for #623.
-
- https://github.com/tomchristie/django-rest-framework/pull/623
- """
- data = {
- 'title': 'x' * 201,
- }
- serializer = ActionItemSerializerCustomRestore(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'title': ['Ensure this value has at most 200 characters (it has 201).']})
-
- def test_default_modelfield_max_length_exceeded(self):
- data = {
- 'title': 'Testing "info" field...',
- 'info': 'x' * 13,
- }
- serializer = ActionItemSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, {'info': ['Ensure this value has at most 12 characters (it has 13).']})
-
- def test_datetime_validation_failure(self):
- """
- Test DateTimeField validation errors on non-str values.
- Regression test for #669.
-
- https://github.com/tomchristie/django-rest-framework/issues/669
- """
- data = self.data
- data['created'] = 0
-
- serializer = CommentSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
-
- self.assertIn('created', serializer.errors)
-
- def test_missing_model_field_exception_msg(self):
- """
- Assert that a meaningful exception message is outputted when the model
- field is missing (e.g. when mistyping ``model``).
- """
- class BrokenModelSerializer(serializers.ModelSerializer):
- class Meta:
- fields = ['some_field']
-
- try:
- BrokenModelSerializer()
- except AssertionError as e:
- self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option")
- except:
- self.fail('Wrong exception type thrown.')
-
- def test_writable_star_source_on_nested_serializer(self):
- """
- Assert that a nested serializer instantiated with source='*' correctly
- expands the data into the outer serializer.
- """
- serializer = ModelSerializerWithNestedSerializer(data={
- 'name': 'marko',
- 'nested': {'info': 'hi'}},
- )
- self.assertEqual(serializer.is_valid(), True)
-
-
-class CustomValidationTests(TestCase):
- class CommentSerializerWithFieldValidator(CommentSerializer):
-
- def validate_email(self, attrs, source):
- attrs[source]
- return attrs
-
- def validate_content(self, attrs, source):
- value = attrs[source]
- if "test" not in value:
- raise serializers.ValidationError("Test not in value")
- return attrs
-
- def test_field_validation(self):
- data = {
- 'email': 'tom@example.com',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertTrue(serializer.is_valid())
-
- data['content'] = 'This should not validate'
-
- serializer = self.CommentSerializerWithFieldValidator(data=data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['Test not in value']})
-
- def test_missing_data(self):
- """
- Make sure that validate_content isn't called if the field is missing
- """
- incomplete_data = {
- 'email': 'tom@example.com',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=incomplete_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'content': ['This field is required.']})
-
- def test_wrong_data(self):
- """
- Make sure that validate_content isn't called if the field input is wrong
- """
- wrong_data = {
- 'email': 'not an email',
- 'content': 'A test comment',
- 'created': datetime.datetime(2012, 1, 1)
- }
- serializer = self.CommentSerializerWithFieldValidator(data=wrong_data)
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']})
-
-
-class PositiveIntegerAsChoiceTests(TestCase):
- def test_positive_integer_in_json_is_correctly_parsed(self):
- data = {'some_integer': 1}
- serializer = PositiveIntegerAsChoiceSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
-
-
-class ModelValidationTests(TestCase):
- def test_validate_unique(self):
- """
- Just check if serializers.ModelSerializer handles unique checks via .full_clean()
- """
- serializer = AlbumsSerializer(data={'title': 'a'})
- serializer.is_valid()
- serializer.save()
- second_serializer = AlbumsSerializer(data={'title': 'a'})
- self.assertFalse(second_serializer.is_valid())
- self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
-
- def test_foreign_key_with_partial(self):
- """
- Test ModelSerializer validation with partial=True
-
- Specifically test foreign key validation.
- """
-
- album = Album(title='test')
- album.save()
-
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
-
- photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk})
- self.assertTrue(photo_serializer.is_valid())
- photo = photo_serializer.save()
-
- # Updating only the album (foreign key)
- photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True)
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
- # Updating only the description
- photo_serializer = PhotoSerializer(instance=photo,
- data={'description': 'new'},
- partial=True)
-
- self.assertTrue(photo_serializer.is_valid())
- self.assertTrue(photo_serializer.save())
-
-
-class RegexValidationTest(TestCase):
- def test_create_failed(self):
- serializer = BookSerializer(data={'isbn': '1234567890'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': '12345678901234'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- serializer = BookSerializer(data={'isbn': 'abcdefghijklm'})
- self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'isbn': ['isbn has to be exact 13 numbers']})
-
- def test_create_success(self):
- serializer = BookSerializer(data={'isbn': '1234567890123'})
- self.assertTrue(serializer.is_valid())
-
-
-class MetadataTests(TestCase):
- def test_empty(self):
- serializer = CommentSerializer()
- expected = {
- 'email': serializers.CharField,
- 'content': serializers.CharField,
- 'created': serializers.DateTimeField
- }
- for field_name, field in expected.items():
- self.assertTrue(isinstance(serializer.data.fields[field_name], field))
-
-
-class ManyToManyTests(TestCase):
- def setUp(self):
- class ManyToManySerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyModel
-
- self.serializer_class = ManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'id': 1, 'rel': [self.anchor.id]}
-
- def test_retrieve(self):
- """
- Serialize an instance of a model with a ManyToMany relationship.
- """
- serializer = self.serializer_class(instance=self.instance)
- expected = self.data
- self.assertEqual(serializer.data, expected)
-
- def test_create(self):
- """
- Create an instance of a model with a ManyToMany relationship.
- """
- data = {'rel': [self.anchor.id]}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update(self):
- """
- Update an instance of a model with a ManyToMany relationship.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [self.anchor, new_anchor])
-
- def test_create_empty_relationship(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- data = {'rel': []}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_update_empty_relationship(self):
- """
- Update an instance of a model with a ManyToMany relationship,
- containing no items.
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': []}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(list(instance.rel.all()), [])
-
- def test_create_empty_relationship_flat_data(self):
- """
- Create an instance of a model with a ManyToMany relationship,
- containing no items, using a representation that does not support
- lists (eg form data).
- """
- data = MultiValueDict()
- data.setlist('rel', [''])
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ManyToManyModel.objects.all()), 2)
- self.assertEqual(instance.pk, 2)
- self.assertEqual(list(instance.rel.all()), [])
-
-
-class ReadOnlyManyToManyTests(TestCase):
- def setUp(self):
- class ReadOnlyManyToManySerializer(serializers.ModelSerializer):
- rel = serializers.RelatedField(many=True, read_only=True)
-
- class Meta:
- model = ReadOnlyManyToManyModel
-
- self.serializer_class = ReadOnlyManyToManySerializer
-
- # An anchor instance to use for the relationship
- self.anchor = Anchor()
- self.anchor.save()
-
- # A model instance with a many to many relationship to the anchor
- self.instance = ReadOnlyManyToManyModel()
- self.instance.save()
- self.instance.rel.add(self.anchor)
-
- # A serialized representation of the model instance
- self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'}
-
- def test_update(self):
- """
- Attempt to update an instance of a model with a ManyToMany
- relationship. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {'rel': [self.anchor.id, new_anchor.id]}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
- def test_update_without_relationship(self):
- """
- Attempt to update an instance of a model where many to ManyToMany
- relationship is not supplied. Not updated due to read_only=True
- """
- new_anchor = Anchor()
- new_anchor.save()
- data = {}
- serializer = self.serializer_class(self.instance, data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(ReadOnlyManyToManyModel.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- # rel is still as original (1 entry)
- self.assertEqual(list(instance.rel.all()), [self.anchor])
-
-
-class DefaultValueTests(TestCase):
- def setUp(self):
- class DefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = DefaultValueModel
-
- self.serializer_class = DefaultValueSerializer
- self.objects = DefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
- def test_partial_update_default(self):
- """ Regression test for issue #532 """
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- data = {'extra': 'extra_value'}
- serializer = self.serializer_class(instance=instance, data=data, partial=True)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
-
- self.assertEqual(instance.extra, 'extra_value')
- self.assertEqual(instance.text, 'overridden')
-
-
-class CallableDefaultValueTests(TestCase):
- def setUp(self):
- class CallableDefaultValueSerializer(serializers.ModelSerializer):
- class Meta:
- model = CallableDefaultValueModel
-
- self.serializer_class = CallableDefaultValueSerializer
- self.objects = CallableDefaultValueModel.objects
-
- def test_create_using_default(self):
- data = {}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'foobar')
-
- def test_create_overriding_default(self):
- data = {'text': 'overridden'}
- serializer = self.serializer_class(data=data)
- self.assertEqual(serializer.is_valid(), True)
- instance = serializer.save()
- self.assertEqual(len(self.objects.all()), 1)
- self.assertEqual(instance.pk, 1)
- self.assertEqual(instance.text, 'overridden')
-
-
-class ManyRelatedTests(TestCase):
- def test_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'title': 'Test blog post',
- 'comments': [
- {'text': 'I hate this blog post'},
- {'text': 'I love this blog post'}
- ]
- }
-
- self.assertEqual(serializer.data, expected)
-
- def test_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post', 'blogpostcomment_set': [1, 2]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_depth_include_reverse_relations(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I hate this blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPost
- fields = ('id', 'title', 'blogpostcomment_set')
- depth = 1
-
- serializer = BlogPostSerializer(instance=post)
- expected = {
- 'id': 1, 'title': 'Test blog post',
- 'blogpostcomment_set': [
- {'id': 1, 'text': 'I hate this blog post', 'blog_post': 1},
- {'id': 2, 'text': 'I love this blog post', 'blog_post': 1}
- ]
- }
- self.assertEqual(serializer.data, expected)
-
- def test_callable_source(self):
- post = BlogPost.objects.create(title="Test blog post")
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class BlogPostCommentSerializer(serializers.Serializer):
- text = serializers.CharField()
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- first_comment = BlogPostCommentSerializer(source='get_first_comment')
-
- serializer = BlogPostSerializer(post)
-
- expected = {
- 'title': 'Test blog post',
- 'first_comment': {'text': 'I love this blog post'}
- }
- self.assertEqual(serializer.data, expected)
-
-
-class RelatedTraversalTest(TestCase):
- def test_nested_traversal(self):
- """
- Source argument should support dotted.source notation.
- """
- user = Person.objects.create(name="django")
- post = BlogPost.objects.create(title="Test blog post", writer=user)
- post.blogpostcomment_set.create(text="I love this blog post")
-
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ("name", "age")
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- fields = ("text", "post_owner")
-
- text = serializers.CharField()
- post_owner = PersonSerializer(source='blog_post.writer')
-
- class BlogPostSerializer(serializers.Serializer):
- title = serializers.CharField()
- comments = BlogPostCommentSerializer(source='blogpostcomment_set')
-
- serializer = BlogPostSerializer(instance=post)
-
- expected = {
- 'title': 'Test blog post',
- 'comments': [{
- 'text': 'I love this blog post',
- 'post_owner': {
- "name": "django",
- "age": None
- }
- }]
- }
-
- self.assertEqual(serializer.data, expected)
-
- def test_nested_traversal_with_none(self):
- """
- If a component of the dotted.source is None, return None for the field.
- """
- from rest_framework.tests.models import NullableForeignKeySource
- instance = NullableForeignKeySource.objects.create(name='Source with null FK')
-
- class NullableSourceSerializer(serializers.Serializer):
- target_name = serializers.Field(source='target.name')
-
- serializer = NullableSourceSerializer(instance=instance)
-
- expected = {
- 'target_name': None,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-class SerializerMethodFieldTests(TestCase):
- def setUp(self):
-
- class BoopSerializer(serializers.Serializer):
- beep = serializers.SerializerMethodField('get_beep')
- boop = serializers.Field()
- boop_count = serializers.SerializerMethodField('get_boop_count')
-
- def get_beep(self, obj):
- return 'hello!'
-
- def get_boop_count(self, obj):
- return len(obj.boop)
-
- self.serializer_class = BoopSerializer
-
- def test_serializer_method_field(self):
-
- class MyModel(object):
- boop = ['a', 'b', 'c']
-
- source_data = MyModel()
-
- serializer = self.serializer_class(source_data)
-
- expected = {
- 'beep': 'hello!',
- 'boop': ['a', 'b', 'c'],
- 'boop_count': 3,
- }
-
- self.assertEqual(serializer.data, expected)
-
-
-# Test for issue #324
-class BlankFieldTests(TestCase):
- def setUp(self):
-
- class BlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlankFieldModel
-
- class BlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField(required=False)
-
- class NotBlankFieldModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = BasicModel
-
- class NotBlankFieldSerializer(serializers.Serializer):
- title = serializers.CharField()
-
- self.model_serializer_class = BlankFieldModelSerializer
- self.serializer_class = BlankFieldSerializer
- self.not_blank_model_serializer_class = NotBlankFieldModelSerializer
- self.not_blank_serializer_class = NotBlankFieldSerializer
- self.data = {'title': ''}
-
- def test_create_blank_field(self):
- serializer = self.serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_blank_field(self):
- serializer = self.model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_model_null_field(self):
- serializer = self.model_serializer_class(data={'title': None})
- self.assertEqual(serializer.is_valid(), True)
-
- def test_create_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a non-model serializer
- """
- serializer = self.not_blank_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_not_blank_field(self):
- """
- Test to ensure blank data in a field not marked as blank=True
- is considered invalid in a model serializer
- """
- serializer = self.not_blank_model_serializer_class(data=self.data)
- self.assertEqual(serializer.is_valid(), False)
-
- def test_create_model_empty_field(self):
- serializer = self.model_serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
-
-#test for issue #460
-class SerializerPickleTests(TestCase):
- """
- Test pickleability of the output of Serializers
- """
- def test_pickle_simple_model_serializer_data(self):
- """
- Test simple serializer
- """
- pickle.dumps(PersonSerializer(Person(name="Methusela", age=969)).data)
-
- def test_pickle_inner_serializer(self):
- """
- Test pickling a serializer whose resulting .data (a SortedDictWithMetadata) will
- have unpickleable meta data--in order to make sure metadata doesn't get pulled into the pickle.
- See DictWithMetadata.__getstate__
- """
- class InnerPersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
- fields = ('name', 'age')
- pickle.dumps(InnerPersonSerializer(Person(name="Noah", age=950)).data, 0)
-
- def test_getstate_method_should_not_return_none(self):
- """
- Regression test for #645.
- """
- data = serializers.DictWithMetadata({1: 1})
- self.assertEqual(data.__getstate__(), serializers.SortedDict({1: 1}))
-
- def test_serializer_data_is_pickleable(self):
- """
- Another regression test for #645.
- """
- data = serializers.SortedDictWithMetadata({1: 1})
- repr(pickle.loads(pickle.dumps(data, 0)))
-
-
-# test for issue #725
-class SeveralChoicesModel(models.Model):
- color = models.CharField(
- max_length=10,
- choices=[('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')],
- blank=False
- )
- drink = models.CharField(
- max_length=10,
- choices=[('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')],
- blank=False,
- default='beer'
- )
- os = models.CharField(
- max_length=10,
- choices=[('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')],
- blank=True
- )
- music_genre = models.CharField(
- max_length=10,
- choices=[('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')],
- blank=True,
- default='metal'
- )
-
-
-class SerializerChoiceFields(TestCase):
-
- def setUp(self):
- super(SerializerChoiceFields, self).setUp()
-
- class SeveralChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = SeveralChoicesModel
- fields = ('color', 'drink', 'os', 'music_genre')
-
- self.several_choices_serializer = SeveralChoicesSerializer
-
- def test_choices_blank_false_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['color'].choices,
- [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]
- )
-
- def test_choices_blank_false_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['drink'].choices,
- [('beer', 'Beer'), ('wine', 'Wine'), ('cider', 'Cider')]
- )
-
- def test_choices_blank_true_not_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['os'].choices,
- BLANK_CHOICE_DASH + [('linux', 'Linux'), ('osx', 'OSX'), ('windows', 'Windows')]
- )
-
- def test_choices_blank_true_with_default(self):
- serializer = self.several_choices_serializer()
- self.assertEqual(
- serializer.fields['music_genre'].choices,
- BLANK_CHOICE_DASH + [('rock', 'Rock'), ('metal', 'Metal'), ('grunge', 'Grunge')]
- )
-
-
-# Regression tests for #675
-class Ticket(models.Model):
- assigned = models.ForeignKey(
- Person, related_name='assigned_tickets')
- reviewer = models.ForeignKey(
- Person, blank=True, null=True, related_name='reviewed_tickets')
-
-
-class SerializerRelatedChoicesTest(TestCase):
-
- def setUp(self):
- super(SerializerRelatedChoicesTest, self).setUp()
-
- class RelatedChoicesSerializer(serializers.ModelSerializer):
- class Meta:
- model = Ticket
- fields = ('assigned', 'reviewer')
-
- self.related_fields_serializer = RelatedChoicesSerializer
-
- def test_empty_queryset_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- []
- )
-
- def test_empty_queryset_not_required(self):
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------')]
- )
-
- def test_with_some_persons_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['assigned'].widget.choices],
- [(1, 'Person object - 1'), (2, 'Person object - 2')]
- )
-
- def test_with_some_persons_not_required(self):
- Person.objects.create(name="Lionel Messi")
- Person.objects.create(name="Xavi Hernandez")
- serializer = self.related_fields_serializer()
- self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
- self.assertEqual(
- [x for x in serializer.fields['reviewer'].widget.choices],
- [('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
- )
-
-
-class DepthTest(TestCase):
- def test_implicit_nesting(self):
-
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- class Meta:
- model = BlogPostComment
- depth = 2
-
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
-
- self.assertEqual(serializer.data, expected)
-
- def test_explicit_nesting(self):
- writer = Person.objects.create(name="django", age=1)
- post = BlogPost.objects.create(title="Test blog post", writer=writer)
- comment = BlogPostComment.objects.create(text="Test blog post comment", blog_post=post)
-
- class PersonSerializer(serializers.ModelSerializer):
- class Meta:
- model = Person
-
- class BlogPostSerializer(serializers.ModelSerializer):
- writer = PersonSerializer()
-
- class Meta:
- model = BlogPost
-
- class BlogPostCommentSerializer(serializers.ModelSerializer):
- blog_post = BlogPostSerializer()
-
- class Meta:
- model = BlogPostComment
-
- serializer = BlogPostCommentSerializer(instance=comment)
- expected = {'id': 1, 'text': 'Test blog post comment', 'blog_post': {'id': 1, 'title': 'Test blog post',
- 'writer': {'id': 1, 'name': 'django', 'age': 1}}}
-
- self.assertEqual(serializer.data, expected)
-
-
-class NestedSerializerContextTests(TestCase):
-
- def test_nested_serializer_context(self):
- """
- Regression for #497
-
- https://github.com/tomchristie/django-rest-framework/issues/497
- """
- class PhotoSerializer(serializers.ModelSerializer):
- class Meta:
- model = Photo
- fields = ("description", "callable")
-
- callable = serializers.SerializerMethodField('_callable')
-
- def _callable(self, instance):
- if not 'context_item' in self.context:
- raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
- return "success"
-
- class AlbumSerializer(serializers.ModelSerializer):
- class Meta:
- model = Album
- fields = ("photo_set", "callable")
-
- photo_set = PhotoSerializer(source="photo_set")
- callable = serializers.SerializerMethodField("_callable")
-
- def _callable(self, instance):
- if not 'context_item' in self.context:
- raise RuntimeError("context isn't getting passed into 1st level nested serializer")
- return "success"
-
- class AlbumCollection(object):
- albums = None
-
- class AlbumCollectionSerializer(serializers.Serializer):
- albums = AlbumSerializer(source="albums")
-
- album1 = Album.objects.create(title="album 1")
- album2 = Album.objects.create(title="album 2")
- Photo.objects.create(description="Bigfoot", album=album1)
- Photo.objects.create(description="Unicorn", album=album1)
- Photo.objects.create(description="Yeti", album=album2)
- Photo.objects.create(description="Sasquatch", album=album2)
- album_collection = AlbumCollection()
- album_collection.albums = [album1, album2]
-
- # This will raise RuntimeError if context doesn't get passed correctly to the nested Serializers
- AlbumCollectionSerializer(album_collection, context={'context_item': 'album context'}).data
-
-
-class DeserializeListTestCase(TestCase):
-
- def setUp(self):
- self.data = {
- 'email': 'nobody@nowhere.com',
- 'content': 'This is some test content',
- 'created': datetime.datetime(2013, 3, 7),
- }
-
- def test_no_errors(self):
- data = [self.data.copy() for x in range(0, 3)]
- serializer = CommentSerializer(data=data, many=True)
- self.assertTrue(serializer.is_valid())
- self.assertTrue(isinstance(serializer.object, list))
- self.assertTrue(
- all((isinstance(item, Comment) for item in serializer.object))
- )
-
- def test_errors_return_as_list(self):
- invalid_item = self.data.copy()
- invalid_item['email'] = ''
- data = [self.data.copy(), invalid_item, self.data.copy()]
-
- serializer = CommentSerializer(data=data, many=True)
- self.assertFalse(serializer.is_valid())
- expected = [{}, {'email': ['This field is required.']}, {}]
- self.assertEqual(serializer.errors, expected)
-
-
-# Test for issue 747
-
-class LazyStringModel(object):
- def __init__(self, lazystring):
- self.lazystring = lazystring
-
-
-class LazyStringSerializer(serializers.Serializer):
- lazystring = serializers.Field()
-
- def restore_object(self, attrs, instance=None):
- if instance is not None:
- instance.lazystring = attrs.get('lazystring', instance.lazystring)
- return instance
- return LazyStringModel(**attrs)
-
-
-class LazyStringsTestCase(TestCase):
- def setUp(self):
- self.model = LazyStringModel(lazystring=_('lazystring'))
-
- def test_lazy_strings_are_translated(self):
- serializer = LazyStringSerializer(self.model)
- self.assertEqual(type(serializer.data['lazystring']),
- type('lazystring'))
-
-
-# Test for issue #467
-
-class FieldLabelTest(TestCase):
- def setUp(self):
- self.serializer_class = BasicModelSerializer
-
- def test_label_from_model(self):
- """
- Validates that label and help_text are correctly copied from the model class.
- """
- serializer = self.serializer_class()
- text_field = serializer.fields['text']
-
- self.assertEqual('Text comes here', text_field.label)
- self.assertEqual('Text description.', text_field.help_text)
-
- def test_field_ctor(self):
- """
- This is check that ctor supports both label and help_text.
- """
- self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label)
- self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text)
- self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
-
-
-# Test for issue #961
-
-class ManyFieldHelpTextTest(TestCase):
- def test_help_text_no_hold_down_control_msg(self):
- """
- Validate that help_text doesn't contain the 'Hold down "Control" ...'
- message that Django appends to choice fields.
- """
- rel_field = fields.Field(help_text=ManyToManyModel._meta.get_field('rel').help_text)
- self.assertEqual('Some help text.', rel_field.help_text)
-
-
-class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
- class AMOAFModel(RESTFrameworkModel):
- char_field = models.CharField(max_length=1024, blank=True)
- comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
- decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
- email_field = models.EmailField(max_length=1024, blank=True)
- file_field = models.FileField(max_length=1024, blank=True)
- image_field = models.ImageField(max_length=1024, blank=True)
- slug_field = models.SlugField(max_length=1024, blank=True)
- url_field = models.URLField(max_length=1024, blank=True)
-
- class AMOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = AMOAFModel
-
- self.serializer_class = AMOAFSerializer
- self.fields_attributes = {
- 'char_field': [
- ('max_length', 1024),
- ],
- 'comma_separated_integer_field': [
- ('max_length', 1024),
- ],
- 'decimal_field': [
- ('max_digits', 64),
- ('decimal_places', 32),
- ],
- 'email_field': [
- ('max_length', 1024),
- ],
- 'file_field': [
- ('max_length', 1024),
- ],
- 'image_field': [
- ('max_length', 1024),
- ],
- 'slug_field': [
- ('max_length', 1024),
- ],
- 'url_field': [
- ('max_length', 1024),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
-
- def test_char_field(self):
- self.field_test('char_field')
-
- def test_comma_separated_integer_field(self):
- self.field_test('comma_separated_integer_field')
-
- def test_decimal_field(self):
- self.field_test('decimal_field')
-
- def test_email_field(self):
- self.field_test('email_field')
-
- def test_file_field(self):
- self.field_test('file_field')
-
- def test_image_field(self):
- self.field_test('image_field')
-
- def test_slug_field(self):
- self.field_test('slug_field')
-
- def test_url_field(self):
- self.field_test('url_field')
-
-
-class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
-
- def setUp(self):
- class DVOAFModel(RESTFrameworkModel):
- positive_integer_field = models.PositiveIntegerField(blank=True)
- positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
- email_field = models.EmailField(blank=True)
- file_field = models.FileField(blank=True)
- image_field = models.ImageField(blank=True)
- slug_field = models.SlugField(blank=True)
- url_field = models.URLField(blank=True)
-
- class DVOAFSerializer(serializers.ModelSerializer):
- class Meta:
- model = DVOAFModel
-
- self.serializer_class = DVOAFSerializer
- self.fields_attributes = {
- 'positive_integer_field': [
- ('min_value', 0),
- ],
- 'positive_small_integer_field': [
- ('min_value', 0),
- ],
- 'email_field': [
- ('max_length', 75),
- ],
- 'file_field': [
- ('max_length', 100),
- ],
- 'image_field': [
- ('max_length', 100),
- ],
- 'slug_field': [
- ('max_length', 50),
- ],
- 'url_field': [
- ('max_length', 200),
- ],
- }
-
- def field_test(self, field):
- serializer = self.serializer_class(data={})
- self.assertEqual(serializer.is_valid(), True)
-
- for attribute in self.fields_attributes[field]:
- self.assertEqual(
- getattr(serializer.fields[field], attribute[0]),
- attribute[1]
- )
-
- def test_positive_integer_field(self):
- self.field_test('positive_integer_field')
-
- def test_positive_small_integer_field(self):
- self.field_test('positive_small_integer_field')
-
- def test_email_field(self):
- self.field_test('email_field')
-
- def test_file_field(self):
- self.field_test('file_field')
-
- def test_image_field(self):
- self.field_test('image_field')
-
- def test_slug_field(self):
- self.field_test('slug_field')
-
- def test_url_field(self):
- self.field_test('url_field')
-
-
-class MetadataSerializer(serializers.Serializer):
- field1 = serializers.CharField(3, required=True)
- field2 = serializers.CharField(10, required=False)
-
-
-class MetadataSerializerTestCase(TestCase):
- def setUp(self):
- self.serializer = MetadataSerializer()
-
- def test_serializer_metadata(self):
- metadata = self.serializer.metadata()
- expected = {
- 'field1': {
- 'required': True,
- 'max_length': 3,
- 'type': 'string',
- 'read_only': False
- },
- 'field2': {
- 'required': False,
- 'max_length': 10,
- 'type': 'string',
- 'read_only': False
- }
- }
- self.assertEqual(expected, metadata)
-
-
-### Regression test for #840
-
-class SimpleModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class SimpleModelSerializer(serializers.ModelSerializer):
- text = serializers.CharField()
- other = serializers.CharField()
-
- class Meta:
- model = SimpleModel
-
- def validate_other(self, attrs, source):
- del attrs['other']
- return attrs
-
-
-class FieldValidationRemovingAttr(TestCase):
- def test_removing_non_model_field_in_validation(self):
- """
- Removing an attr during field valiation should ensure that it is not
- passed through when restoring the object.
-
- This allows additional non-model fields to be supported.
-
- Regression test for #840.
- """
- serializer = SimpleModelSerializer(data={'text': 'foo', 'other': 'bar'})
- self.assertTrue(serializer.is_valid())
- serializer.save()
- self.assertEqual(serializer.object.text, 'foo')
-
-
-### Regression test for #878
-
-class SimpleTargetModel(models.Model):
- text = models.CharField(max_length=100)
-
-
-class SimplePKSourceModelSerializer(serializers.Serializer):
- targets = serializers.PrimaryKeyRelatedField(queryset=SimpleTargetModel.objects.all(), many=True)
- text = serializers.CharField()
-
-
-class SimpleSlugSourceModelSerializer(serializers.Serializer):
- targets = serializers.SlugRelatedField(queryset=SimpleTargetModel.objects.all(), many=True, slug_field='pk')
- text = serializers.CharField()
-
-
-class SerializerSupportsManyRelationships(TestCase):
- def setUp(self):
- SimpleTargetModel.objects.create(text='foo')
- SimpleTargetModel.objects.create(text='bar')
-
- def test_serializer_supports_pk_many_relationships(self):
- """
- Regression test for #878.
-
- Note that pk behavior has a different code path to usual cases,
- for performance reasons.
- """
- serializer = SimplePKSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
-
- def test_serializer_supports_slug_many_relationships(self):
- """
- Regression test for #878.
- """
- serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]})
- self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]})
diff --git a/rest_framework/tests/test_serializer_bulk_update.py b/rest_framework/tests/test_serializer_bulk_update.py
deleted file mode 100644
index 8b0ded1a..00000000
--- a/rest_framework/tests/test_serializer_bulk_update.py
+++ /dev/null
@@ -1,278 +0,0 @@
-"""
-Tests to cover bulk create and update using serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-
-
-class BulkCreateSerializerTests(TestCase):
- """
- Creating multiple instances using serializers.
- """
-
- def setUp(self):
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- self.BookSerializer = BookSerializer
-
- def test_bulk_create_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_bulk_create_errors(self):
- """
- Correct bulk update serialization should return the input data.
- """
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 'foo',
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {},
- {'id': ['Enter a whole number.']}
- ]
-
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_list_datatype(self):
- """
- Data containing list of incorrect data type should return errors.
- """
- data = ['foo', 'bar', 'baz']
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = [
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']}
- ]
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_datatype(self):
- """
- Data containing a single incorrect data type should return errors.
- """
- data = 123
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_invalid_single_object(self):
- """
- Data containing only a single object, instead of a list of objects
- should return errors.
- """
- data = {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }
- serializer = self.BookSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
-
- expected_errors = {'non_field_errors': ['Expected a list of items.']}
-
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class BulkUpdateSerializerTests(TestCase):
- """
- Updating multiple instances using serializers.
- """
-
- def setUp(self):
- class Book(object):
- """
- A data type that can be persisted to a mock storage backend
- with `.save()` and `.delete()`.
- """
- object_map = {}
-
- def __init__(self, id, title, author):
- self.id = id
- self.title = title
- self.author = author
-
- def save(self):
- Book.object_map[self.id] = self
-
- def delete(self):
- del Book.object_map[self.id]
-
- class BookSerializer(serializers.Serializer):
- id = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- author = serializers.CharField(max_length=100)
-
- def restore_object(self, attrs, instance=None):
- if instance:
- instance.id = attrs['id']
- instance.title = attrs['title']
- instance.author = attrs['author']
- return instance
- return Book(**attrs)
-
- self.Book = Book
- self.BookSerializer = BookSerializer
-
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 1,
- 'title': 'If this is a man',
- 'author': 'Primo Levi'
- }, {
- 'id': 2,
- 'title': 'The wind-up bird chronicle',
- 'author': 'Haruki Murakami'
- }
- ]
-
- for item in data:
- book = Book(item['id'], item['title'], item['author'])
- book.save()
-
- def books(self):
- """
- Return all the objects in the mock storage backend.
- """
- return self.Book.object_map.values()
-
- def test_bulk_update_success(self):
- """
- Correct bulk update serialization should return the input data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 2,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
-
- self.assertEqual(data, new_data)
-
- def test_bulk_update_and_create(self):
- """
- Bulk update serialization may also include created items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.data, data)
- serializer.save()
- new_data = self.BookSerializer(self.books(), many=True).data
- self.assertEqual(data, new_data)
-
- def test_bulk_update_invalid_create(self):
- """
- Bulk update serialization without allow_add_remove may not create items.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 3,
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'non_field_errors': ['Cannot create a new item, only existing items may be updated.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_bulk_update_error(self):
- """
- Incorrect bulk update serialization should return error data.
- """
- data = [
- {
- 'id': 0,
- 'title': 'The electric kool-aid acid test',
- 'author': 'Tom Wolfe'
- }, {
- 'id': 'foo',
- 'title': 'Kafka on the shore',
- 'author': 'Haruki Murakami'
- }
- ]
- expected_errors = [
- {},
- {'id': ['Enter a whole number.']}
- ]
- serializer = self.BookSerializer(self.books(), data=data, many=True, allow_add_remove=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py
deleted file mode 100644
index 71d0e24b..00000000
--- a/rest_framework/tests/test_serializer_nested.py
+++ /dev/null
@@ -1,246 +0,0 @@
-"""
-Tests to cover nested serializers.
-
-Doesn't cover model serializers.
-"""
-from __future__ import unicode_literals
-from django.test import TestCase
-from rest_framework import serializers
-
-
-class WritableNestedSerializerBasicTests(TestCase):
- """
- Tests for deserializing nested entities.
- Basic tests that use serializers that simply restore to dicts.
- """
-
- def setUp(self):
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, data)
-
- def test_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- expected_errors = {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
- def test_many_nested_validation_error(self):
- """
- Incorrect nested serialization should return appropriate error data
- when multiple entities are being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 'foobar'}
- ]
- }
- ]
- expected_errors = [
- {},
- {
- 'tracks': [
- {},
- {},
- {'duration': ['Enter a whole number.']}
- ]
- }
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), False)
- self.assertEqual(serializer.errors, expected_errors)
-
-
-class WritableNestedSerializerObjectTests(TestCase):
- """
- Tests for deserializing nested entities.
- These tests use serializers that restore to concrete objects.
- """
-
- def setUp(self):
- # Couple of concrete objects that we're going to deserialize into
- class Track(object):
- def __init__(self, order, title, duration):
- self.order, self.title, self.duration = order, title, duration
-
- def __eq__(self, other):
- return (
- self.order == other.order and
- self.title == other.title and
- self.duration == other.duration
- )
-
- class Album(object):
- def __init__(self, album_name, artist, tracks):
- self.album_name, self.artist, self.tracks = album_name, artist, tracks
-
- def __eq__(self, other):
- return (
- self.album_name == other.album_name and
- self.artist == other.artist and
- self.tracks == other.tracks
- )
-
- # And their corresponding serializers
- class TrackSerializer(serializers.Serializer):
- order = serializers.IntegerField()
- title = serializers.CharField(max_length=100)
- duration = serializers.IntegerField()
-
- def restore_object(self, attrs, instance=None):
- return Track(attrs['order'], attrs['title'], attrs['duration'])
-
- class AlbumSerializer(serializers.Serializer):
- album_name = serializers.CharField(max_length=100)
- artist = serializers.CharField(max_length=100)
- tracks = TrackSerializer(many=True)
-
- def restore_object(self, attrs, instance=None):
- return Album(attrs['album_name'], attrs['artist'], attrs['tracks'])
-
- self.Album, self.Track = Album, Track
- self.AlbumSerializer = AlbumSerializer
-
- def test_nested_validation_success(self):
- """
- Correct nested serialization should return a restored object
- that corresponds to the input data.
- """
-
- data = {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- expected_object = self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
-
- serializer = self.AlbumSerializer(data=data)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
-
- def test_many_nested_validation_success(self):
- """
- Correct nested serialization should return multiple restored objects
- that corresponds to the input data when multiple objects are
- being deserialized.
- """
-
- data = [
- {
- 'album_name': 'Russian Red',
- 'artist': 'I Love Your Glasses',
- 'tracks': [
- {'order': 1, 'title': 'Cigarettes', 'duration': 121},
- {'order': 2, 'title': 'No Past Land', 'duration': 198},
- {'order': 3, 'title': 'They Don\'t Believe', 'duration': 191}
- ]
- },
- {
- 'album_name': 'Discovery',
- 'artist': 'Daft Punk',
- 'tracks': [
- {'order': 1, 'title': 'One More Time', 'duration': 235},
- {'order': 2, 'title': 'Aerodynamic', 'duration': 184},
- {'order': 3, 'title': 'Digital Love', 'duration': 239}
- ]
- }
- ]
- expected_object = [
- self.Album(
- album_name='Russian Red',
- artist='I Love Your Glasses',
- tracks=[
- self.Track(order=1, title='Cigarettes', duration=121),
- self.Track(order=2, title='No Past Land', duration=198),
- self.Track(order=3, title='They Don\'t Believe', duration=191),
- ]
- ),
- self.Album(
- album_name='Discovery',
- artist='Daft Punk',
- tracks=[
- self.Track(order=1, title='One More Time', duration=235),
- self.Track(order=2, title='Aerodynamic', duration=184),
- self.Track(order=3, title='Digital Love', duration=239),
- ]
- )
- ]
-
- serializer = self.AlbumSerializer(data=data, many=True)
- self.assertEqual(serializer.is_valid(), True)
- self.assertEqual(serializer.object, expected_object)
diff --git a/rest_framework/tests/test_settings.py b/rest_framework/tests/test_settings.py
deleted file mode 100644
index 857375c2..00000000
--- a/rest_framework/tests/test_settings.py
+++ /dev/null
@@ -1,22 +0,0 @@
-"""Tests for the settings module"""
-from __future__ import unicode_literals
-from django.test import TestCase
-
-from rest_framework.settings import APISettings, DEFAULTS, IMPORT_STRINGS
-
-
-class TestSettings(TestCase):
- """Tests relating to the api settings"""
-
- def test_non_import_errors(self):
- """Make sure other errors aren't suppressed."""
- settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.bad_import.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
- with self.assertRaises(ValueError):
- settings.DEFAULT_MODEL_SERIALIZER_CLASS
-
- def test_import_error_message_maintained(self):
- """Make sure real import errors are captured and raised sensibly."""
- settings = APISettings({'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.tests.extras.not_here.ModelSerializer'}, DEFAULTS, IMPORT_STRINGS)
- with self.assertRaises(ImportError) as cm:
- settings.DEFAULT_MODEL_SERIALIZER_CLASS
- self.assertTrue('ImportError' in str(cm.exception))
diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py
deleted file mode 100644
index 49d45fc2..00000000
--- a/rest_framework/tests/test_testing.py
+++ /dev/null
@@ -1,115 +0,0 @@
-# -- coding: utf-8 --
-
-from __future__ import unicode_literals
-from django.contrib.auth.models import User
-from django.test import TestCase
-from rest_framework.compat import patterns, url
-from rest_framework.decorators import api_view
-from rest_framework.response import Response
-from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
-
-
-@api_view(['GET', 'POST'])
-def view(request):
- return Response({
- 'auth': request.META.get('HTTP_AUTHORIZATION', b''),
- 'user': request.user.username
- })
-
-
-urlpatterns = patterns('',
- url(r'^view/$', view),
-)
-
-
-class TestAPITestClient(TestCase):
- urls = 'rest_framework.tests.test_testing'
-
- def setUp(self):
- self.client = APIClient()
-
- def test_credentials(self):
- """
- Setting `.credentials()` adds the required headers to each request.
- """
- self.client.credentials(HTTP_AUTHORIZATION='example')
- for _ in range(0, 3):
- response = self.client.get('/view/')
- self.assertEqual(response.data['auth'], 'example')
-
- def test_force_authenticate(self):
- """
- Setting `.force_authenticate()` forcibly authenticates each request.
- """
- user = User.objects.create_user('example', 'example@example.com')
- self.client.force_authenticate(user)
- response = self.client.get('/view/')
- self.assertEqual(response.data['user'], 'example')
-
- def test_csrf_exempt_by_default(self):
- """
- By default, the test client is CSRF exempt.
- """
- User.objects.create_user('example', 'example@example.com', 'password')
- self.client.login(username='example', password='password')
- response = self.client.post('/view/')
- self.assertEqual(response.status_code, 200)
-
- def test_explicitly_enforce_csrf_checks(self):
- """
- The test client can enforce CSRF checks.
- """
- client = APIClient(enforce_csrf_checks=True)
- User.objects.create_user('example', 'example@example.com', 'password')
- client.login(username='example', password='password')
- response = client.post('/view/')
- expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
- self.assertEqual(response.status_code, 403)
- self.assertEqual(response.data, expected)
-
-
-class TestAPIRequestFactory(TestCase):
- def test_csrf_exempt_by_default(self):
- """
- By default, the test client is CSRF exempt.
- """
- user = User.objects.create_user('example', 'example@example.com', 'password')
- factory = APIRequestFactory()
- request = factory.post('/view/')
- request.user = user
- response = view(request)
- self.assertEqual(response.status_code, 200)
-
- def test_explicitly_enforce_csrf_checks(self):
- """
- The test client can enforce CSRF checks.
- """
- user = User.objects.create_user('example', 'example@example.com', 'password')
- factory = APIRequestFactory(enforce_csrf_checks=True)
- request = factory.post('/view/')
- request.user = user
- response = view(request)
- expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
- self.assertEqual(response.status_code, 403)
- self.assertEqual(response.data, expected)
-
- def test_invalid_format(self):
- """
- Attempting to use a format that is not configured will raise an
- assertion error.
- """
- factory = APIRequestFactory()
- self.assertRaises(AssertionError, factory.post,
- path='/view/', data={'example': 1}, format='xml'
- )
-
- def test_force_authenticate(self):
- """
- Setting `force_authenticate()` forcibly authenticates the request.
- """
- user = User.objects.create_user('example', 'example@example.com')
- factory = APIRequestFactory()
- request = factory.get('/view')
- force_authenticate(request, user=user)
- response = view(request)
- self.assertEqual(response.data['user'], 'example')
diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py
deleted file mode 100644
index ebfdff9c..00000000
--- a/rest_framework/tests/test_validation.py
+++ /dev/null
@@ -1,85 +0,0 @@
-from __future__ import unicode_literals
-from django.db import models
-from django.test import TestCase
-from rest_framework import generics, serializers, status
-from rest_framework.test import APIRequestFactory
-
-factory = APIRequestFactory()
-
-
-# Regression for #666
-
-class ValidationModel(models.Model):
- blank_validated_field = models.CharField(max_length=255)
-
-
-class ValidationModelSerializer(serializers.ModelSerializer):
- class Meta:
- model = ValidationModel
- fields = ('blank_validated_field',)
- read_only_fields = ('blank_validated_field',)
-
-
-class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
- model = ValidationModel
- serializer_class = ValidationModelSerializer
-
-
-class TestPreSaveValidationExclusions(TestCase):
- def test_pre_save_validation_exclusions(self):
- """
- Somewhat weird test case to ensure that we don't perform model
- validation on read only fields.
- """
- obj = ValidationModel.objects.create(blank_validated_field='')
- request = factory.put('/', {}, format='json')
- view = UpdateValidationModel().as_view()
- response = view(request, pk=obj.pk).render()
- self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-
-# Regression for #653
-
-class ShouldValidateModel(models.Model):
- should_validate_field = models.CharField(max_length=255)
-
-
-class ShouldValidateModelSerializer(serializers.ModelSerializer):
- renamed = serializers.CharField(source='should_validate_field', required=False)
-
- class Meta:
- model = ShouldValidateModel
- fields = ('renamed',)
-
-
-class TestPreSaveValidationExclusions(TestCase):
- def test_renamed_fields_are_model_validated(self):
- """
- Ensure fields with 'source' applied do get still get model validation.
- """
- # We've set `required=False` on the serializer, but the model
- # does not have `blank=True`, so this serializer should not validate.
- serializer = ShouldValidateModelSerializer(data={'renamed': ''})
- self.assertEqual(serializer.is_valid(), False)
-
-
-class ValidationSerializer(serializers.Serializer):
- foo = serializers.CharField()
-
- def validate_foo(self, attrs, source):
- raise serializers.ValidationError("foo invalid")
-
- def validate(self, attrs):
- raise serializers.ValidationError("serializer invalid")
-
-
-class TestAvoidValidation(TestCase):
- """
- If serializer was initialized with invalid data (None or non dict-like), it
- should avoid validation layer (validate_<field> and validate methods)
- """
- def test_serializer_errors_has_only_invalid_data_error(self):
- serializer = ValidationSerializer(data='invalid data')
- self.assertFalse(serializer.is_valid())
- self.assertDictEqual(serializer.errors,
- {'non_field_errors': ['Invalid data']})
diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py
deleted file mode 100644
index 554ebd1a..00000000
--- a/rest_framework/tests/tests.py
+++ /dev/null
@@ -1,16 +0,0 @@
-"""
-Force import of all modules in this package in order to get the standard test
-runner to pick up the tests. Yowzers.
-"""
-from __future__ import unicode_literals
-import os
-import django
-
-modules = [filename.rsplit('.', 1)[0]
- for filename in os.listdir(os.path.dirname(__file__))
- if filename.endswith('.py') and not filename.startswith('_')]
-__test__ = dict()
-
-if django.VERSION < (1, 6):
- for module in modules:
- exec("from rest_framework.tests.%s import *" % module)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index f6bb1cc8..261fc246 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -2,7 +2,7 @@
Provides various throttling policies.
"""
from __future__ import unicode_literals
-from django.core.cache import cache
+from django.core.cache import cache as default_cache
from django.core.exceptions import ImproperlyConfigured
from rest_framework.settings import api_settings
import time
@@ -18,6 +18,25 @@ class BaseThrottle(object):
"""
raise NotImplementedError('.allow_request() must be overridden')
+ def get_ident(self, request):
+ """
+ Identify the machine making the request by parsing HTTP_X_FORWARDED_FOR
+ if present and number of proxies is > 0. If not use all of
+ HTTP_X_FORWARDED_FOR if it is available, if not use REMOTE_ADDR.
+ """
+ xff = request.META.get('HTTP_X_FORWARDED_FOR')
+ remote_addr = request.META.get('REMOTE_ADDR')
+ num_proxies = api_settings.NUM_PROXIES
+
+ if num_proxies is not None:
+ if num_proxies == 0 or xff is None:
+ return remote_addr
+ addrs = xff.split(',')
+ client_addr = addrs[-min(num_proxies, len(addrs))]
+ return client_addr.strip()
+
+ return ''.join(xff.split()) if xff else remote_addr
+
def wait(self):
"""
Optionally, return a recommended number of seconds to wait before
@@ -39,8 +58,9 @@ class SimpleRateThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache.
"""
+ cache = default_cache
timer = time.time
- cache_format = 'throtte_%(scope)s_%(ident)s'
+ cache_format = 'throttle_%(scope)s_%(ident)s'
scope = None
THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES
@@ -96,7 +116,10 @@ class SimpleRateThrottle(BaseThrottle):
return True
self.key = self.get_cache_key(request, view)
- self.history = cache.get(self.key, [])
+ if self.key is None:
+ return True
+
+ self.history = self.cache.get(self.key, [])
self.now = self.timer()
# Drop any requests from the history which have now passed the
@@ -113,7 +136,7 @@ class SimpleRateThrottle(BaseThrottle):
into the cache.
"""
self.history.insert(0, self.now)
- cache.set(self.key, self.history, self.duration)
+ self.cache.set(self.key, self.history, self.duration)
return True
def throttle_failure(self):
@@ -132,6 +155,8 @@ class SimpleRateThrottle(BaseThrottle):
remaining_duration = self.duration
available_requests = self.num_requests - len(self.history) + 1
+ if available_requests <= 0:
+ return None
return remaining_duration / float(available_requests)
@@ -148,11 +173,9 @@ class AnonRateThrottle(SimpleRateThrottle):
if request.user.is_authenticated():
return None # Only throttle unauthenticated requests.
- ident = request.META.get('REMOTE_ADDR', None)
-
return self.cache_format % {
'scope': self.scope,
- 'ident': ident
+ 'ident': self.get_ident(request)
}
@@ -168,9 +191,9 @@ class UserRateThrottle(SimpleRateThrottle):
def get_cache_key(self, request, view):
if request.user.is_authenticated():
- ident = request.user.id
+ ident = request.user.pk
else:
- ident = request.META.get('REMOTE_ADDR', None)
+ ident = self.get_ident(request)
return self.cache_format % {
'scope': self.scope,
@@ -216,9 +239,9 @@ class ScopedRateThrottle(SimpleRateThrottle):
with the '.throttle_scope` property of the view.
"""
if request.user.is_authenticated():
- ident = request.user.id
+ ident = request.user.pk
else:
- ident = request.META.get('REMOTE_ADDR', None)
+ ident = self.get_ident(request)
return self.cache_format % {
'scope': self.scope,
diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py
index d9143bb4..038e9ee3 100644
--- a/rest_framework/urlpatterns.py
+++ b/rest_framework/urlpatterns.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
+from django.conf.urls import url, include
from django.core.urlresolvers import RegexURLResolver
-from rest_framework.compat import url, include
from rest_framework.settings import api_settings
@@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None):
allowed_pattern = '(%s)' % '|'.join(allowed)
suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern)
else:
- suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg
+ suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg
return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required)
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 9c4719f1..cfcee534 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -2,23 +2,25 @@
Login and logout views for the browsable API.
Add these to your root URLconf if you're using the browsable API and
-your API requires authentication.
-
-The urls must be namespaced as 'rest_framework', and you should make sure
-your authentication settings include `SessionAuthentication`.
+your API requires authentication:
urlpatterns = patterns('',
...
- url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
)
+
+The urls must be namespaced as 'rest_framework', and you should make sure
+your authentication settings include `SessionAuthentication`.
"""
from __future__ import unicode_literals
-from rest_framework.compat import patterns, url
+from django.conf.urls import patterns, url
+from django.contrib.auth import views
template_name = {'template_name': 'rest_framework/login.html'}
-urlpatterns = patterns('django.contrib.auth.views',
- url(r'^login/$', 'login', template_name, name='login'),
- url(r'^logout/$', 'logout', template_name, name='logout'),
+urlpatterns = patterns(
+ '',
+ url(r'^login/$', views.login, template_name, name='login'),
+ url(r'^logout/$', views.logout, template_name, name='logout')
)
diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py
index d51374b0..e6690d17 100644
--- a/rest_framework/utils/breadcrumbs.py
+++ b/rest_framework/utils/breadcrumbs.py
@@ -1,6 +1,5 @@
from __future__ import unicode_literals
from django.core.urlresolvers import resolve, get_script_prefix
-from rest_framework.utils.formatting import get_view_name
def get_breadcrumbs(url):
@@ -9,8 +8,11 @@ def get_breadcrumbs(url):
tuple of (name, url).
"""
+ from rest_framework.settings import api_settings
from rest_framework.views import APIView
+ view_name_func = api_settings.VIEW_NAME_FUNCTION
+
def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen):
"""
Add tuples of (name, url) to the breadcrumbs list,
@@ -30,7 +32,7 @@ def get_breadcrumbs(url):
# Probably an optional trailing slash.
if not seen or seen[-1] != view:
suffix = getattr(view, 'suffix', None)
- name = get_view_name(view.cls, suffix)
+ name = view_name_func(cls, suffix)
breadcrumbs_list.insert(0, (name, prefix + url))
seen.append(view)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index b26a2085..2160d18b 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -2,96 +2,60 @@
Helper classes for parsers.
"""
from __future__ import unicode_literals
-from django.utils.datastructures import SortedDict
+from django.db.models.query import QuerySet
+from django.utils import six, timezone
+from django.utils.encoding import force_text
from django.utils.functional import Promise
-from rest_framework.compat import timezone, force_text
-from rest_framework.serializers import DictWithMetadata, SortedDictWithMetadata
+from rest_framework.compat import total_seconds
import datetime
import decimal
-import types
import json
+import uuid
class JSONEncoder(json.JSONEncoder):
"""
JSONEncoder subclass that knows how to encode date/time/timedelta,
- decimal types, and generators.
+ decimal types, generators and other basic python objects.
"""
- def default(self, o):
+ def default(self, obj):
# For Date Time string spec, see ECMA 262
# http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
- if isinstance(o, Promise):
- return force_text(o)
- elif isinstance(o, datetime.datetime):
- r = o.isoformat()
- if o.microsecond:
- r = r[:23] + r[26:]
- if r.endswith('+00:00'):
- r = r[:-6] + 'Z'
- return r
- elif isinstance(o, datetime.date):
- return o.isoformat()
- elif isinstance(o, datetime.time):
- if timezone and timezone.is_aware(o):
+ if isinstance(obj, Promise):
+ return force_text(obj)
+ elif isinstance(obj, datetime.datetime):
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:23] + representation[26:]
+ if representation.endswith('+00:00'):
+ representation = representation[:-6] + 'Z'
+ return representation
+ elif isinstance(obj, datetime.date):
+ return obj.isoformat()
+ elif isinstance(obj, datetime.time):
+ if timezone and timezone.is_aware(obj):
raise ValueError("JSON can't represent timezone-aware times.")
- r = o.isoformat()
- if o.microsecond:
- r = r[:12]
- return r
- elif isinstance(o, datetime.timedelta):
- return str(o.total_seconds())
- elif isinstance(o, decimal.Decimal):
- return str(o)
- elif hasattr(o, '__iter__'):
- return [i for i in o]
- return super(JSONEncoder, self).default(o)
-
-
-try:
- import yaml
-except ImportError:
- SafeDumper = None
-else:
- # Adapted from http://pyyaml.org/attachment/ticket/161/use_ordered_dict.py
- class SafeDumper(yaml.SafeDumper):
- """
- Handles decimals as strings.
- Handles SortedDicts as usual dicts, but preserves field order, rather
- than the usual behaviour of sorting the keys.
- """
- def represent_decimal(self, data):
- return self.represent_scalar('tag:yaml.org,2002:str', str(data))
-
- def represent_mapping(self, tag, mapping, flow_style=None):
- value = []
- node = yaml.MappingNode(tag, value, flow_style=flow_style)
- if self.alias_key is not None:
- self.represented_objects[self.alias_key] = node
- best_style = True
- if hasattr(mapping, 'items'):
- mapping = list(mapping.items())
- if not isinstance(mapping, SortedDict):
- mapping.sort()
- for item_key, item_value in mapping:
- node_key = self.represent_data(item_key)
- node_value = self.represent_data(item_value)
- if not (isinstance(node_key, yaml.ScalarNode) and not node_key.style):
- best_style = False
- if not (isinstance(node_value, yaml.ScalarNode) and not node_value.style):
- best_style = False
- value.append((node_key, node_value))
- if flow_style is None:
- if self.default_flow_style is not None:
- node.flow_style = self.default_flow_style
- else:
- node.flow_style = best_style
- return node
-
- SafeDumper.add_representer(SortedDict,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(DictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(SortedDictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(types.GeneratorType,
- yaml.representer.SafeRepresenter.represent_list)
+ representation = obj.isoformat()
+ if obj.microsecond:
+ representation = representation[:12]
+ return representation
+ elif isinstance(obj, datetime.timedelta):
+ return six.text_type(total_seconds(obj))
+ elif isinstance(obj, decimal.Decimal):
+ # Serializers will coerce decimals to strings by default.
+ return float(obj)
+ elif isinstance(obj, uuid.UUID):
+ return six.text_type(obj)
+ elif isinstance(obj, QuerySet):
+ return tuple(obj)
+ elif hasattr(obj, 'tolist'):
+ # Numpy arrays and array scalars.
+ return obj.tolist()
+ elif hasattr(obj, '__getitem__'):
+ try:
+ return dict(obj)
+ except:
+ pass
+ elif hasattr(obj, '__iter__'):
+ return tuple(item for item in obj)
+ return super(JSONEncoder, self).default(obj)
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
new file mode 100644
index 00000000..c97ec5d0
--- /dev/null
+++ b/rest_framework/utils/field_mapping.py
@@ -0,0 +1,249 @@
+"""
+Helper functions for mapping model fields to a dictionary of default
+keyword arguments that should be used for their equivelent serializer fields.
+"""
+from django.core import validators
+from django.db import models
+from django.utils.text import capfirst
+from rest_framework.compat import clean_manytomany_helptext
+from rest_framework.validators import UniqueValidator
+import inspect
+
+
+NUMERIC_FIELD_TYPES = (
+ models.IntegerField, models.FloatField, models.DecimalField
+)
+
+
+class ClassLookupDict(object):
+ """
+ Takes a dictionary with classes as keys.
+ Lookups against this object will traverses the object's inheritance
+ hierarchy in method resolution order, and returns the first matching value
+ from the dictionary or raises a KeyError if nothing matches.
+ """
+ def __init__(self, mapping):
+ self.mapping = mapping
+
+ def __getitem__(self, key):
+ if hasattr(key, '_proxy_class'):
+ # Deal with proxy classes. Ie. BoundField behaves as if it
+ # is a Field instance when using ClassLookupDict.
+ base_class = key._proxy_class
+ else:
+ base_class = key.__class__
+
+ for cls in inspect.getmro(base_class):
+ if cls in self.mapping:
+ return self.mapping[cls]
+ raise KeyError('Class %s not found in lookup.', cls.__name__)
+
+ def __setitem__(self, key, value):
+ self.mapping[key] = value
+
+
+def needs_label(model_field, field_name):
+ """
+ Returns `True` if the label based on the model's verbose name
+ is not equal to the default label it would have based on it's field name.
+ """
+ default_label = field_name.replace('_', ' ').capitalize()
+ return capfirst(model_field.verbose_name) != default_label
+
+
+def get_detail_view_name(model):
+ """
+ Given a model class, return the view name to use for URL relationships
+ that refer to instances of the model.
+ """
+ return '%(model_name)s-detail' % {
+ 'app_label': model._meta.app_label,
+ 'model_name': model._meta.object_name.lower()
+ }
+
+
+def get_field_kwargs(field_name, model_field):
+ """
+ Creates a default instance of a basic non-relational field.
+ """
+ kwargs = {}
+ validator_kwarg = list(model_field.validators)
+
+ # The following will only be used by ModelField classes.
+ # Gets removed for everything else.
+ kwargs['model_field'] = model_field
+
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+
+ if model_field.help_text:
+ kwargs['help_text'] = model_field.help_text
+
+ max_digits = getattr(model_field, 'max_digits', None)
+ if max_digits is not None:
+ kwargs['max_digits'] = max_digits
+
+ decimal_places = getattr(model_field, 'decimal_places', None)
+ if decimal_places is not None:
+ kwargs['decimal_places'] = decimal_places
+
+ if isinstance(model_field, models.TextField):
+ kwargs['style'] = {'base_template': 'textarea.html'}
+
+ if isinstance(model_field, models.AutoField) or not model_field.editable:
+ # If this field is read-only, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['read_only'] = True
+ return kwargs
+
+ if model_field.has_default() or model_field.blank or model_field.null:
+ kwargs['required'] = False
+
+ if model_field.null and not isinstance(model_field, models.NullBooleanField):
+ kwargs['allow_null'] = True
+
+ if model_field.blank:
+ kwargs['allow_blank'] = True
+
+ if model_field.flatchoices:
+ # If this model field contains choices, then return early.
+ # Further keyword arguments are not valid.
+ kwargs['choices'] = model_field.flatchoices
+ return kwargs
+
+ # Ensure that max_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_length = getattr(model_field, 'max_length', None)
+ if max_length is not None and isinstance(model_field, models.CharField):
+ kwargs['max_length'] = max_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxLengthValidator)
+ ]
+
+ # Ensure that min_length is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_length = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinLengthValidator)
+ ), None)
+ if min_length is not None and isinstance(model_field, models.CharField):
+ kwargs['min_length'] = min_length
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinLengthValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ max_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MaxValueValidator)
+ ), None)
+ if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
+ kwargs['max_value'] = max_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MaxValueValidator)
+ ]
+
+ # Ensure that max_value is passed explicitly as a keyword arg,
+ # rather than as a validator.
+ min_value = next((
+ validator.limit_value for validator in validator_kwarg
+ if isinstance(validator, validators.MinValueValidator)
+ ), None)
+ if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
+ kwargs['min_value'] = min_value
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.MinValueValidator)
+ ]
+
+ # URLField does not need to include the URLValidator argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.URLField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if not isinstance(validator, validators.URLValidator)
+ ]
+
+ # EmailField does not need to include the validate_email argument,
+ # as it is explicitly added in.
+ if isinstance(model_field, models.EmailField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_email
+ ]
+
+ # SlugField do not need to include the 'validate_slug' argument,
+ if isinstance(model_field, models.SlugField):
+ validator_kwarg = [
+ validator for validator in validator_kwarg
+ if validator is not validators.validate_slug
+ ]
+
+ if getattr(model_field, 'unique', False):
+ validator = UniqueValidator(queryset=model_field.model._default_manager)
+ validator_kwarg.append(validator)
+
+ if validator_kwarg:
+ kwargs['validators'] = validator_kwarg
+
+ return kwargs
+
+
+def get_relation_kwargs(field_name, relation_info):
+ """
+ Creates a default instance of a flat relational field.
+ """
+ model_field, related_model, to_many, has_through_model = relation_info
+ kwargs = {
+ 'queryset': related_model._default_manager,
+ 'view_name': get_detail_view_name(related_model)
+ }
+
+ if to_many:
+ kwargs['many'] = True
+
+ if has_through_model:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+
+ if model_field:
+ if model_field.verbose_name and needs_label(model_field, field_name):
+ kwargs['label'] = capfirst(model_field.verbose_name)
+ help_text = clean_manytomany_helptext(model_field.help_text)
+ if help_text:
+ kwargs['help_text'] = help_text
+ if not model_field.editable:
+ kwargs['read_only'] = True
+ kwargs.pop('queryset', None)
+ if kwargs.get('read_only', False):
+ # If this field is read-only, then return early.
+ # No further keyword arguments are valid.
+ return kwargs
+ if model_field.has_default() or model_field.null:
+ kwargs['required'] = False
+ if model_field.null:
+ kwargs['allow_null'] = True
+ if model_field.validators:
+ kwargs['validators'] = model_field.validators
+ if getattr(model_field, 'unique', False):
+ validator = UniqueValidator(queryset=model_field.model._default_manager)
+ kwargs['validators'] = kwargs.get('validators', []) + [validator]
+
+ return kwargs
+
+
+def get_nested_relation_kwargs(relation_info):
+ kwargs = {'read_only': True}
+ if relation_info.to_many:
+ kwargs['many'] = True
+ return kwargs
+
+
+def get_url_kwargs(model_field):
+ return {
+ 'view_name': get_detail_view_name(model_field)
+ }
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 4bec8387..8b6f005e 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -2,14 +2,13 @@
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
-
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown, smart_text
+from rest_framework.compat import apply_markdown, force_text
import re
-def _remove_trailing_string(content, trailing):
+def remove_trailing_string(content, trailing):
"""
Strip trailing component `trailing` from `content` if it exists.
Used when generating names from view classes.
@@ -19,11 +18,16 @@ def _remove_trailing_string(content, trailing):
return content
-def _remove_leading_indent(content):
+def dedent(content):
"""
Remove leading indent from a block of text.
Used when generating descriptions from docstrings.
+
+ Note that python's `textwrap.dedent` doesn't quite cut it,
+ as it fails to dedent multiline docstrings that include
+ unindented text on the initial line.
"""
+ content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
@@ -31,11 +35,11 @@ def _remove_leading_indent(content):
if whitespace_counts:
whitespace_pattern = '^' + (' ' * min(whitespace_counts))
content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content)
- content = content.strip('\n')
- return content
+ return content.strip()
-def _camelcase_to_spaces(content):
+
+def camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes.
@@ -45,30 +49,6 @@ def _camelcase_to_spaces(content):
return ' '.join(content.split('_')).title()
-def get_view_name(cls, suffix=None):
- """
- Return a formatted name for an `APIView` class or `@api_view` function.
- """
- name = cls.__name__
- name = _remove_trailing_string(name, 'View')
- name = _remove_trailing_string(name, 'ViewSet')
- name = _camelcase_to_spaces(name)
- if suffix:
- name += ' ' + suffix
- return name
-
-
-def get_view_description(cls, html=False):
- """
- Return a description for an `APIView` class or `@api_view` function.
- """
- description = cls.__doc__ or ''
- description = _remove_leading_indent(smart_text(description))
- if html:
- return markup_description(description)
- return description
-
-
def markup_description(description):
"""
Apply HTML markup to the given description.
@@ -77,4 +57,5 @@ def markup_description(description):
description = apply_markdown(description)
else:
description = escape(description).replace('\n', '<br />')
+ description = '<p>' + description + '</p>'
return mark_safe(description)
diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py
new file mode 100644
index 00000000..d773952d
--- /dev/null
+++ b/rest_framework/utils/html.py
@@ -0,0 +1,88 @@
+"""
+Helpers for dealing with HTML input.
+"""
+import re
+from django.utils.datastructures import MultiValueDict
+
+
+def is_html_input(dictionary):
+ # MultiDict type datastructures are used to represent HTML form input,
+ # which may have more than one value for each key.
+ return hasattr(dictionary, 'getlist')
+
+
+def parse_html_list(dictionary, prefix=''):
+ """
+ Used to suport list values in HTML forms.
+ Supports lists of primitives and/or dictionaries.
+
+ * List of primitives.
+
+ {
+ '[0]': 'abc',
+ '[1]': 'def',
+ '[2]': 'hij'
+ }
+ -->
+ [
+ 'abc',
+ 'def',
+ 'hij'
+ ]
+
+ * List of dictionaries.
+
+ {
+ '[0]foo': 'abc',
+ '[0]bar': 'def',
+ '[1]foo': 'hij',
+ '[1]bar': 'klm',
+ }
+ -->
+ [
+ {'foo': 'abc', 'bar': 'def'},
+ {'foo': 'hij', 'bar': 'klm'}
+ ]
+ """
+ ret = {}
+ regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
+ for field, value in dictionary.items():
+ match = regex.match(field)
+ if not match:
+ continue
+ index, key = match.groups()
+ index = int(index)
+ if not key:
+ ret[index] = value
+ elif isinstance(ret.get(index), dict):
+ ret[index][key] = value
+ else:
+ ret[index] = MultiValueDict({key: [value]})
+ return [ret[item] for item in sorted(ret.keys())]
+
+
+def parse_html_dict(dictionary, prefix):
+ """
+ Used to support dictionary values in HTML forms.
+
+ {
+ 'profile.username': 'example',
+ 'profile.email': 'example@example.com',
+ }
+ -->
+ {
+ 'profile': {
+ 'username': 'example',
+ 'email': 'example@example.com'
+ }
+ }
+ """
+ ret = {}
+ regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
+ for field, value in dictionary.items():
+ match = regex.match(field)
+ if not match:
+ continue
+ key = match.groups()[0]
+ ret[key] = value
+ return ret
diff --git a/rest_framework/utils/humanize_datetime.py b/rest_framework/utils/humanize_datetime.py
new file mode 100644
index 00000000..649f2abc
--- /dev/null
+++ b/rest_framework/utils/humanize_datetime.py
@@ -0,0 +1,47 @@
+"""
+Helper functions that convert strftime formats into more readable representations.
+"""
+from rest_framework import ISO_8601
+
+
+def datetime_formats(formats):
+ format = ', '.join(formats).replace(
+ ISO_8601,
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
+ )
+ return humanize_strptime(format)
+
+
+def date_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'YYYY[-MM[-DD]]')
+ return humanize_strptime(format)
+
+
+def time_formats(formats):
+ format = ', '.join(formats).replace(ISO_8601, 'hh:mm[:ss[.uuuuuu]]')
+ return humanize_strptime(format)
+
+
+def humanize_strptime(format_string):
+ # Note that we're missing some of the locale specific mappings that
+ # don't really make sense.
+ mapping = {
+ "%Y": "YYYY",
+ "%y": "YY",
+ "%m": "MM",
+ "%b": "[Jan-Dec]",
+ "%B": "[January-December]",
+ "%d": "DD",
+ "%H": "hh",
+ "%I": "hh", # Requires '%p' to differentiate from '%H'.
+ "%M": "mm",
+ "%S": "ss",
+ "%f": "uuuuuu",
+ "%a": "[Mon-Sun]",
+ "%A": "[Monday-Sunday]",
+ "%p": "[AM|PM]",
+ "%z": "[+HHMM|-HHMM]"
+ }
+ for key, val in mapping.items():
+ format_string = format_string.replace(key, val)
+ return format_string
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index c09c2933..de2931c2 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -5,6 +5,7 @@ See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
"""
from __future__ import unicode_literals
from django.http.multipartparser import parse_header
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import HTTP_HEADER_ENCODING
@@ -43,6 +44,7 @@ def order_by_precedence(media_type_lst):
return [media_types for media_types in ret if media_types]
+@python_2_unicode_compatible
class _MediaType(object):
def __init__(self, media_type_str):
if media_type_str is None:
@@ -57,7 +59,7 @@ class _MediaType(object):
if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
return False
- if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
+ if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
return False
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
@@ -74,14 +76,11 @@ class _MediaType(object):
return 0
elif self.sub_type == '*':
return 1
- elif not self.params or self.params.keys() == ['q']:
+ elif not self.params or list(self.params.keys()) == ['q']:
return 2
return 3
def __str__(self):
- return unicode(self).encode('utf-8')
-
- def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
for key, val in self.params.items():
ret += "; %s=%s" % (key, val)
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
new file mode 100644
index 00000000..d92bceb9
--- /dev/null
+++ b/rest_framework/utils/model_meta.py
@@ -0,0 +1,169 @@
+"""
+Helper function for returning the field information that is associated
+with a model class. This includes returning all the forward and reverse
+relationships and their associated metadata.
+
+Usage: `get_field_info(model)` returns a `FieldInfo` instance.
+"""
+from collections import namedtuple
+from django.core.exceptions import ImproperlyConfigured
+from django.db import models
+from django.utils import six
+from rest_framework.compat import OrderedDict
+import inspect
+
+
+FieldInfo = namedtuple('FieldResult', [
+ 'pk', # Model field instance
+ 'fields', # Dict of field name -> model field instance
+ 'forward_relations', # Dict of field name -> RelationInfo
+ 'reverse_relations', # Dict of field name -> RelationInfo
+ 'fields_and_pk', # Shortcut for 'pk' + 'fields'
+ 'relations' # Shortcut for 'forward_relations' + 'reverse_relations'
+])
+
+RelationInfo = namedtuple('RelationInfo', [
+ 'model_field',
+ 'related_model',
+ 'to_many',
+ 'has_through_model'
+])
+
+
+def _resolve_model(obj):
+ """
+ Resolve supplied `obj` to a Django model class.
+
+ `obj` must be a Django model class itself, or a string
+ representation of one. Useful in situations like GH #1225 where
+ Django may not have resolved a string-based reference to a model in
+ another model's foreign key definition.
+
+ String representations should have the format:
+ 'appname.ModelName'
+ """
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
+ app_name, model_name = obj.split('.')
+ resolved_model = models.get_model(app_name, model_name)
+ if resolved_model is None:
+ msg = "Django did not return a model for {0}.{1}"
+ raise ImproperlyConfigured(msg.format(app_name, model_name))
+ return resolved_model
+ elif inspect.isclass(obj) and issubclass(obj, models.Model):
+ return obj
+ raise ValueError("{0} is not a Django model".format(obj))
+
+
+def get_field_info(model):
+ """
+ Given a model class, returns a `FieldInfo` instance, which is a
+ `namedtuple`, containing metadata about the various field types on the model
+ including information about their relationships.
+ """
+ opts = model._meta.concrete_model._meta
+
+ pk = _get_pk(opts)
+ fields = _get_fields(opts)
+ forward_relations = _get_forward_relationships(opts)
+ reverse_relations = _get_reverse_relationships(opts)
+ fields_and_pk = _merge_fields_and_pk(pk, fields)
+ relationships = _merge_relationships(forward_relations, reverse_relations)
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations,
+ fields_and_pk, relationships)
+
+
+def _get_pk(opts):
+ pk = opts.pk
+ while pk.rel and pk.rel.parent_link:
+ # If model is a child via multi-table inheritance, use parent's pk.
+ pk = pk.rel.to._meta.pk
+
+ return pk
+
+
+def _get_fields(opts):
+ fields = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and not field.rel]:
+ fields[field.name] = field
+
+ return fields
+
+
+def _get_forward_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
+ forward_relations = OrderedDict()
+ for field in [field for field in opts.fields if field.serialize and field.rel]:
+ forward_relations[field.name] = RelationInfo(
+ model_field=field,
+ related_model=_resolve_model(field.rel.to),
+ to_many=False,
+ has_through_model=False
+ )
+
+ # Deal with forward many-to-many relationships.
+ for field in [field for field in opts.many_to_many if field.serialize]:
+ forward_relations[field.name] = RelationInfo(
+ model_field=field,
+ related_model=_resolve_model(field.rel.to),
+ to_many=True,
+ has_through_model=(
+ not field.rel.through._meta.auto_created
+ )
+ )
+
+ return forward_relations
+
+
+def _get_reverse_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
+ # Note that we have a hack here to handle internal API differences for
+ # this internal API across Django 1.7 -> Django 1.8.
+ # See: https://code.djangoproject.com/ticket/24208
+
+ reverse_relations = OrderedDict()
+ for relation in opts.get_all_related_objects():
+ accessor_name = relation.get_accessor_name()
+ related = getattr(relation, 'related_model', relation.model)
+ reverse_relations[accessor_name] = RelationInfo(
+ model_field=None,
+ related_model=related,
+ to_many=relation.field.rel.multiple,
+ has_through_model=False
+ )
+
+ # Deal with reverse many-to-many relationships.
+ for relation in opts.get_all_related_many_to_many_objects():
+ accessor_name = relation.get_accessor_name()
+ related = getattr(relation, 'related_model', relation.model)
+ reverse_relations[accessor_name] = RelationInfo(
+ model_field=None,
+ related_model=related,
+ to_many=True,
+ has_through_model=(
+ (getattr(relation.field.rel, 'through', None) is not None) and
+ not relation.field.rel.through._meta.auto_created
+ )
+ )
+
+ return reverse_relations
+
+
+def _merge_fields_and_pk(pk, fields):
+ fields_and_pk = OrderedDict()
+ fields_and_pk['pk'] = pk
+ fields_and_pk[pk.name] = pk
+ fields_and_pk.update(fields)
+
+ return fields_and_pk
+
+
+def _merge_relationships(forward_relations, reverse_relations):
+ return OrderedDict(
+ list(forward_relations.items()) +
+ list(reverse_relations.items())
+ )
diff --git a/rest_framework/utils/representation.py b/rest_framework/utils/representation.py
new file mode 100644
index 00000000..1bfc64c1
--- /dev/null
+++ b/rest_framework/utils/representation.py
@@ -0,0 +1,99 @@
+"""
+Helper functions for creating user-friendly representations
+of serializer classes and serializer fields.
+"""
+from __future__ import unicode_literals
+from django.db import models
+from django.utils.encoding import force_text
+from django.utils.functional import Promise
+from rest_framework.compat import unicode_repr
+import re
+
+
+def manager_repr(value):
+ model = value.model
+ opts = model._meta
+ for _, name, manager in opts.concrete_managers + opts.abstract_managers:
+ if manager == value:
+ return '%s.%s.all()' % (model._meta.object_name, name)
+ return repr(value)
+
+
+def smart_repr(value):
+ if isinstance(value, models.Manager):
+ return manager_repr(value)
+
+ if isinstance(value, Promise) and value._delegate_text:
+ value = force_text(value)
+
+ value = unicode_repr(value)
+
+ # Representations like u'help text'
+ # should simply be presented as 'help text'
+ if value.startswith("u'") and value.endswith("'"):
+ return value[1:]
+
+ # Representations like
+ # <django.core.validators.RegexValidator object at 0x1047af050>
+ # Should be presented as
+ # <django.core.validators.RegexValidator object>
+ value = re.sub(' at 0x[0-9a-f]{4,32}>', '>', value)
+
+ return value
+
+
+def field_repr(field, force_many=False):
+ kwargs = field._kwargs
+ if force_many:
+ kwargs = kwargs.copy()
+ kwargs['many'] = True
+ kwargs.pop('child', None)
+
+ arg_string = ', '.join([smart_repr(val) for val in field._args])
+ kwarg_string = ', '.join([
+ '%s=%s' % (key, smart_repr(val))
+ for key, val in sorted(kwargs.items())
+ ])
+ if arg_string and kwarg_string:
+ arg_string += ', '
+
+ if force_many:
+ class_name = force_many.__class__.__name__
+ else:
+ class_name = field.__class__.__name__
+
+ return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
+
+
+def serializer_repr(serializer, indent, force_many=None):
+ ret = field_repr(serializer, force_many) + ':'
+ indent_str = ' ' * indent
+
+ if force_many:
+ fields = force_many.fields
+ else:
+ fields = serializer.fields
+
+ for field_name, field in fields.items():
+ ret += '\n' + indent_str + field_name + ' = '
+ if hasattr(field, 'fields'):
+ ret += serializer_repr(field, indent + 1)
+ elif hasattr(field, 'child'):
+ ret += list_repr(field, indent + 1)
+ elif hasattr(field, 'child_relation'):
+ ret += field_repr(field.child_relation, force_many=field.child_relation)
+ else:
+ ret += field_repr(field)
+
+ if serializer.validators:
+ ret += '\n' + indent_str + 'class Meta:'
+ ret += '\n' + indent_str + ' validators = ' + smart_repr(serializer.validators)
+
+ return ret
+
+
+def list_repr(serializer, indent):
+ child = serializer.child
+ if hasattr(child, 'fields'):
+ return serializer_repr(serializer, indent, force_many=child)
+ return field_repr(serializer)
diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py
new file mode 100644
index 00000000..87bb3ac0
--- /dev/null
+++ b/rest_framework/utils/serializer_helpers.py
@@ -0,0 +1,120 @@
+from __future__ import unicode_literals
+import collections
+from rest_framework.compat import OrderedDict, unicode_to_repr
+
+
+class ReturnDict(OrderedDict):
+ """
+ Return object from `serialier.data` for the `Serializer` class.
+ Includes a backlink to the serializer instance for renderers
+ to use if they need richer field information.
+ """
+ def __init__(self, *args, **kwargs):
+ self.serializer = kwargs.pop('serializer')
+ super(ReturnDict, self).__init__(*args, **kwargs)
+
+ def copy(self):
+ return ReturnDict(self, serializer=self.serializer)
+
+ def __repr__(self):
+ return dict.__repr__(self)
+
+ def __reduce__(self):
+ # Pickling these objects will drop the .serializer backlink,
+ # but preserve the raw data.
+ return (dict, (dict(self),))
+
+
+class ReturnList(list):
+ """
+ Return object from `serialier.data` for the `SerializerList` class.
+ Includes a backlink to the serializer instance for renderers
+ to use if they need richer field information.
+ """
+ def __init__(self, *args, **kwargs):
+ self.serializer = kwargs.pop('serializer')
+ super(ReturnList, self).__init__(*args, **kwargs)
+
+ def __repr__(self):
+ return list.__repr__(self)
+
+ def __reduce__(self):
+ # Pickling these objects will drop the .serializer backlink,
+ # but preserve the raw data.
+ return (list, (list(self),))
+
+
+class BoundField(object):
+ """
+ A field object that also includes `.value` and `.error` properties.
+ Returned when iterating over a serializer instance,
+ providing an API similar to Django forms and form fields.
+ """
+ def __init__(self, field, value, errors, prefix=''):
+ self._field = field
+ self.value = value
+ self.errors = errors
+ self.name = prefix + self.field_name
+
+ def __getattr__(self, attr_name):
+ return getattr(self._field, attr_name)
+
+ @property
+ def _proxy_class(self):
+ return self._field.__class__
+
+ def __repr__(self):
+ return unicode_to_repr('<%s value=%s errors=%s>' % (
+ self.__class__.__name__, self.value, self.errors
+ ))
+
+
+class NestedBoundField(BoundField):
+ """
+ This `BoundField` additionally implements __iter__ and __getitem__
+ in order to support nested bound fields. This class is the type of
+ `BoundField` that is used for serializer fields.
+ """
+ def __iter__(self):
+ for field in self.fields.values():
+ yield self[field.field_name]
+
+ def __getitem__(self, key):
+ field = self.fields[key]
+ value = self.value.get(key) if self.value else None
+ error = self.errors.get(key) if self.errors else None
+ if hasattr(field, 'fields'):
+ return NestedBoundField(field, value, error, prefix=self.name + '.')
+ return BoundField(field, value, error, prefix=self.name + '.')
+
+
+class BindingDict(collections.MutableMapping):
+ """
+ This dict-like object is used to store fields on a serializer.
+
+ This ensures that whenever fields are added to the serializer we call
+ `field.bind()` so that the `field_name` and `parent` attributes
+ can be set correctly.
+ """
+ def __init__(self, serializer):
+ self.serializer = serializer
+ self.fields = OrderedDict()
+
+ def __setitem__(self, key, field):
+ self.fields[key] = field
+ field.bind(field_name=key, parent=self.serializer)
+
+ def __getitem__(self, key):
+ return self.fields[key]
+
+ def __delitem__(self, key):
+ del self.fields[key]
+
+ def __iter__(self):
+ return iter(self.fields)
+
+ def __len__(self):
+ return len(self.fields)
+
+ def __repr__(self):
+ return dict.__repr__(self.fields)
diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py
new file mode 100644
index 00000000..880ef9ed
--- /dev/null
+++ b/rest_framework/utils/urls.py
@@ -0,0 +1,25 @@
+from django.utils.six.moves.urllib import parse as urlparse
+
+
+def replace_query_param(url, key, val):
+ """
+ Given a URL and a key/val pair, set or replace an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = urlparse.parse_qs(query)
+ query_dict[key] = [val]
+ query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
+
+
+def remove_query_param(url, key):
+ """
+ Given a URL and a key/val pair, remove an item in the query
+ parameters of the URL, and return the new URL.
+ """
+ (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
+ query_dict = urlparse.parse_qs(query)
+ query_dict.pop(key, None)
+ query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
+ return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
new file mode 100644
index 00000000..6ae80b89
--- /dev/null
+++ b/rest_framework/validators.py
@@ -0,0 +1,261 @@
+"""
+We perform uniqueness checks explicitly on the serializer class, rather
+the using Django's `.full_clean()`.
+
+This gives us better separation of concerns, allows us to use single-step
+object creation, and makes it possible to switch between using the implicit
+`ModelSerializer` class and an equivalent explicit `Serializer` class.
+"""
+from __future__ import unicode_literals
+from django.utils.translation import ugettext_lazy as _
+from rest_framework.compat import unicode_to_repr
+from rest_framework.exceptions import ValidationError
+from rest_framework.utils.representation import smart_repr
+
+
+class UniqueValidator(object):
+ """
+ Validator that corresponds to `unique=True` on a model field.
+
+ Should be applied to an individual field on the serializer.
+ """
+ message = _('This field must be unique.')
+
+ def __init__(self, queryset, message=None):
+ self.queryset = queryset
+ self.serializer_field = None
+ self.message = message or self.message
+
+ def set_context(self, serializer_field):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the underlying model field name. This may not be the
+ # same as the serializer field name if `source=<>` is set.
+ self.field_name = serializer_field.source_attrs[0]
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer_field.parent, 'instance', None)
+
+ def filter_queryset(self, value, queryset):
+ """
+ Filter the queryset to all instances matching the given attribute.
+ """
+ filter_kwargs = {self.field_name: value}
+ return queryset.filter(**filter_kwargs)
+
+ def exclude_current_instance(self, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, value):
+ queryset = self.queryset
+ queryset = self.filter_queryset(value, queryset)
+ queryset = self.exclude_current_instance(queryset)
+ if queryset.exists():
+ raise ValidationError(self.message)
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset)
+ ))
+
+
+class UniqueTogetherValidator(object):
+ """
+ Validator that corresponds to `unique_together = (...)` on a model class.
+
+ Should be applied to the serializer class, not to an individual field.
+ """
+ message = _('The fields {field_names} must make a unique set.')
+ missing_message = _('This field is required.')
+
+ def __init__(self, queryset, fields, message=None):
+ self.queryset = queryset
+ self.fields = fields
+ self.serializer_field = None
+ self.message = message or self.message
+
+ def set_context(self, serializer):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer, 'instance', None)
+
+ def enforce_required_fields(self, attrs):
+ """
+ The `UniqueTogetherValidator` always forces an implied 'required'
+ state on the fields it applies to.
+ """
+ if self.instance is not None:
+ return
+
+ missing = dict([
+ (field_name, self.missing_message)
+ for field_name in self.fields
+ if field_name not in attrs
+ ])
+ if missing:
+ raise ValidationError(missing)
+
+ def filter_queryset(self, attrs, queryset):
+ """
+ Filter the queryset to all instances matching the given attributes.
+ """
+ # If this is an update, then any unprovided field should
+ # have it's value set based on the existing instance attribute.
+ if self.instance is not None:
+ for field_name in self.fields:
+ if field_name not in attrs:
+ attrs[field_name] = getattr(self.instance, field_name)
+
+ # Determine the filter keyword arguments and filter the queryset.
+ filter_kwargs = dict([
+ (field_name, attrs[field_name])
+ for field_name in self.fields
+ ])
+ return queryset.filter(**filter_kwargs)
+
+ def exclude_current_instance(self, attrs, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, attrs):
+ self.enforce_required_fields(attrs)
+ queryset = self.queryset
+ queryset = self.filter_queryset(attrs, queryset)
+ queryset = self.exclude_current_instance(attrs, queryset)
+
+ # Ignore validation if any field is None
+ checked_values = [
+ value for field, value in attrs.items() if field in self.fields
+ ]
+ if None not in checked_values and queryset.exists():
+ field_names = ', '.join(self.fields)
+ raise ValidationError(self.message.format(field_names=field_names))
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset),
+ smart_repr(self.fields)
+ ))
+
+
+class BaseUniqueForValidator(object):
+ message = None
+ missing_message = _('This field is required.')
+
+ def __init__(self, queryset, field, date_field, message=None):
+ self.queryset = queryset
+ self.field = field
+ self.date_field = date_field
+ self.message = message or self.message
+
+ def set_context(self, serializer):
+ """
+ This hook is called by the serializer instance,
+ prior to the validation call being made.
+ """
+ # Determine the underlying model field names. These may not be the
+ # same as the serializer field names if `source=<>` is set.
+ self.field_name = serializer.fields[self.field].source_attrs[0]
+ self.date_field_name = serializer.fields[self.date_field].source_attrs[0]
+ # Determine the existing instance, if this is an update operation.
+ self.instance = getattr(serializer, 'instance', None)
+
+ def enforce_required_fields(self, attrs):
+ """
+ The `UniqueFor<Range>Validator` classes always force an implied
+ 'required' state on the fields they are applied to.
+ """
+ missing = dict([
+ (field_name, self.missing_message)
+ for field_name in [self.field, self.date_field]
+ if field_name not in attrs
+ ])
+ if missing:
+ raise ValidationError(missing)
+
+ def filter_queryset(self, attrs, queryset):
+ raise NotImplementedError('`filter_queryset` must be implemented.')
+
+ def exclude_current_instance(self, attrs, queryset):
+ """
+ If an instance is being updated, then do not include
+ that instance itself as a uniqueness conflict.
+ """
+ if self.instance is not None:
+ return queryset.exclude(pk=self.instance.pk)
+ return queryset
+
+ def __call__(self, attrs):
+ self.enforce_required_fields(attrs)
+ queryset = self.queryset
+ queryset = self.filter_queryset(attrs, queryset)
+ queryset = self.exclude_current_instance(attrs, queryset)
+ if queryset.exists():
+ message = self.message.format(date_field=self.date_field)
+ raise ValidationError({self.field: message})
+
+ def __repr__(self):
+ return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % (
+ self.__class__.__name__,
+ smart_repr(self.queryset),
+ smart_repr(self.field),
+ smart_repr(self.date_field)
+ ))
+
+
+class UniqueForDateValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" date.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__day' % self.date_field_name] = date.day
+ filter_kwargs['%s__month' % self.date_field_name] = date.month
+ filter_kwargs['%s__year' % self.date_field_name] = date.year
+ return queryset.filter(**filter_kwargs)
+
+
+class UniqueForMonthValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" month.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__month' % self.date_field_name] = date.month
+ return queryset.filter(**filter_kwargs)
+
+
+class UniqueForYearValidator(BaseUniqueForValidator):
+ message = _('This field must be unique for the "{date_field}" year.')
+
+ def filter_queryset(self, attrs, queryset):
+ value = attrs[self.field]
+ date = attrs[self.date_field]
+
+ filter_kwargs = {}
+ filter_kwargs[self.field_name] = value
+ filter_kwargs['%s__year' % self.date_field_name] = date.year
+ return queryset.filter(**filter_kwargs)
diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py
new file mode 100644
index 00000000..51b886f3
--- /dev/null
+++ b/rest_framework/versioning.py
@@ -0,0 +1,177 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import exceptions
+from rest_framework.compat import unicode_http_header
+from rest_framework.reverse import _reverse
+from rest_framework.settings import api_settings
+from rest_framework.templatetags.rest_framework import replace_query_param
+from rest_framework.utils.mediatypes import _MediaType
+import re
+
+
+class BaseVersioning(object):
+ default_version = api_settings.DEFAULT_VERSION
+ allowed_versions = api_settings.ALLOWED_VERSIONS
+ version_param = api_settings.VERSION_PARAM
+
+ def determine_version(self, request, *args, **kwargs):
+ msg = '{cls}.determine_version() must be implemented.'
+ raise NotImplementedError(msg.format(
+ cls=self.__class__.__name__
+ ))
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ return _reverse(viewname, args, kwargs, request, format, **extra)
+
+ def is_allowed_version(self, version):
+ if not self.allowed_versions:
+ return True
+ return (version == self.default_version) or (version in self.allowed_versions)
+
+
+class AcceptHeaderVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json; version=1.0
+ """
+ invalid_version_message = _('Invalid version in "Accept" header.')
+
+ def determine_version(self, request, *args, **kwargs):
+ media_type = _MediaType(request.accepted_media_type)
+ version = media_type.params.get(self.version_param, self.default_version)
+ version = unicode_http_header(version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotAcceptable(self.invalid_version_message)
+ return version
+
+ # We don't need to implement `reverse`, as the versioning is based
+ # on the `Accept` header, not on the request URL.
+
+
+class URLPathVersioning(BaseVersioning):
+ """
+ To the client this is the same style as `NamespaceVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL keyword arguments to determine the version.
+
+ An example URL conf for two views that accept two different versions.
+
+ urlpatterns = [
+ url(r'^(?P<version>{v1,v2})/users/$', users_list, name='users-list'),
+ url(r'^(?P<version>{v1,v2})/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in URL path.')
+
+ def determine_version(self, request, *args, **kwargs):
+ version = kwargs.get(self.version_param, self.default_version)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ kwargs = {} if (kwargs is None) else kwargs
+ kwargs[self.version_param] = request.version
+
+ return super(URLPathVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+
+
+class NamespaceVersioning(BaseVersioning):
+ """
+ To the client this is the same style as `URLPathVersioning`.
+ The difference is in the backend - this implementation uses
+ Django's URL namespaces to determine the version.
+
+ An example URL conf that is namespaced into two seperate versions
+
+ # users/urls.py
+ urlpatterns = [
+ url(r'^/users/$', users_list, name='users-list'),
+ url(r'^/users/(?P<pk>[0-9]+)/$', users_detail, name='users-detail')
+ ]
+
+ # urls.py
+ urlpatterns = [
+ url(r'^v1/', include('users.urls', namespace='v1')),
+ url(r'^v2/', include('users.urls', namespace='v2'))
+ ]
+
+ GET /1.0/something/ HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in URL path.')
+
+ def determine_version(self, request, *args, **kwargs):
+ resolver_match = getattr(request, 'resolver_match', None)
+ if (resolver_match is None or not resolver_match.namespace):
+ return self.default_version
+ version = resolver_match.namespace
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ if request.version is not None:
+ viewname = self.get_versioned_viewname(viewname, request)
+ return super(NamespaceVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+
+ def get_versioned_viewname(self, viewname, request):
+ return request.version + ':' + viewname
+
+
+class HostNameVersioning(BaseVersioning):
+ """
+ GET /something/ HTTP/1.1
+ Host: v1.example.com
+ Accept: application/json
+ """
+ hostname_regex = re.compile(r'^([a-zA-Z0-9]+)\.[a-zA-Z0-9]+\.[a-zA-Z0-9]+$')
+ invalid_version_message = _('Invalid version in hostname.')
+
+ def determine_version(self, request, *args, **kwargs):
+ hostname, seperator, port = request.get_host().partition(':')
+ match = self.hostname_regex.match(hostname)
+ if not match:
+ return self.default_version
+ version = match.group(1)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ # We don't need to implement `reverse`, as the hostname will already be
+ # preserved as part of the REST framework `reverse` implementation.
+
+
+class QueryParameterVersioning(BaseVersioning):
+ """
+ GET /something/?version=0.1 HTTP/1.1
+ Host: example.com
+ Accept: application/json
+ """
+ invalid_version_message = _('Invalid version in query parameter.')
+
+ def determine_version(self, request, *args, **kwargs):
+ version = request.query_params.get(self.version_param)
+ if not self.is_allowed_version(version):
+ raise exceptions.NotFound(self.invalid_version_message)
+ return version
+
+ def reverse(self, viewname, args=None, kwargs=None, request=None, format=None, **extra):
+ url = super(QueryParameterVersioning, self).reverse(
+ viewname, args, kwargs, request, format, **extra
+ )
+ if request.version is not None:
+ return replace_query_param(url, self.version_param, request.version)
+ return url
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 37bba7f0..b4abc4d9 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -2,28 +2,106 @@
Provides an APIView class that is the base of all views in REST framework.
"""
from __future__ import unicode_literals
-
from django.core.exceptions import PermissionDenied
from django.http import Http404
-from django.utils.datastructures import SortedDict
+from django.utils import six
+from django.utils.encoding import smart_text
+from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from rest_framework import status, exceptions
-from rest_framework.compat import View, HttpResponseBase
+from rest_framework.compat import HttpResponseBase, View
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.settings import api_settings
-from rest_framework.utils.formatting import get_view_name, get_view_description
+from rest_framework.utils import formatting
+import inspect
+import warnings
+
+
+def get_view_name(view_cls, suffix=None):
+ """
+ Given a view class, return a textual name to represent the view.
+ This name is used in the browsable API, and in OPTIONS responses.
+
+ This function is the default for the `VIEW_NAME_FUNCTION` setting.
+ """
+ name = view_cls.__name__
+ name = formatting.remove_trailing_string(name, 'View')
+ name = formatting.remove_trailing_string(name, 'ViewSet')
+ name = formatting.camelcase_to_spaces(name)
+ if suffix:
+ name += ' ' + suffix
+
+ return name
+
+
+def get_view_description(view_cls, html=False):
+ """
+ Given a view class, return a textual description to represent the view.
+ This name is used in the browsable API, and in OPTIONS responses.
+
+ This function is the default for the `VIEW_DESCRIPTION_FUNCTION` setting.
+ """
+ description = view_cls.__doc__ or ''
+ description = formatting.dedent(smart_text(description))
+ if html:
+ return formatting.markup_description(description)
+ return description
+
+
+def exception_handler(exc, context):
+ """
+ Returns the response that should be used for any given exception.
+
+ By default we handle the REST framework `APIException`, and also
+ Django's built-in `ValidationError`, `Http404` and `PermissionDenied`
+ exceptions.
+
+ Any unhandled exceptions may return `None`, which will cause a 500 error
+ to be raised.
+ """
+ if isinstance(exc, exceptions.APIException):
+ headers = {}
+ if getattr(exc, 'auth_header', None):
+ headers['WWW-Authenticate'] = exc.auth_header
+ if getattr(exc, 'wait', None):
+ headers['Retry-After'] = '%d' % exc.wait
+
+ if isinstance(exc.detail, (list, dict)):
+ data = exc.detail
+ else:
+ data = {'detail': exc.detail}
+
+ return Response(data, status=exc.status_code, headers=headers)
+
+ elif isinstance(exc, Http404):
+ msg = _('Not found.')
+ data = {'detail': six.text_type(msg)}
+ return Response(data, status=status.HTTP_404_NOT_FOUND)
+
+ elif isinstance(exc, PermissionDenied):
+ msg = _('Permission denied.')
+ data = {'detail': six.text_type(msg)}
+ return Response(data, status=status.HTTP_403_FORBIDDEN)
+
+ # Note: Unhandled exceptions will raise a 500 error.
+ return None
class APIView(View):
- settings = api_settings
+ # The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
+ metadata_class = api_settings.DEFAULT_METADATA_CLASS
+ versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
+
+ # Allow dependency injection of other settings to make testing easier.
+ settings = api_settings
@classmethod
def as_view(cls, **initkwargs):
@@ -35,7 +113,9 @@ class APIView(View):
"""
view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
- return view
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
+ return csrf_exempt(view)
@property
def allowed_methods(self):
@@ -46,12 +126,12 @@ class APIView(View):
@property
def default_response_headers(self):
- # TODO: deprecate?
- # TODO: Only vary by accept if multiple renderers
- return {
+ headers = {
'Allow': ', '.join(self.allowed_methods),
- 'Vary': 'Accept'
}
+ if len(self.renderer_classes) > 1:
+ headers['Vary'] = 'Accept'
+ return headers
def http_method_not_allowed(self, request, *args, **kwargs):
"""
@@ -64,7 +144,7 @@ class APIView(View):
"""
If request is not permitted, determine what kind of exception to raise.
"""
- if not self.request.successful_authenticator:
+ if not request.successful_authenticator:
raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied()
@@ -88,8 +168,8 @@ class APIView(View):
Returns a dict that is passed through to Parser.parse(),
as the `parser_context` keyword argument.
"""
- # Note: Additionally `request` will also be added to the context
- # by the Request object.
+ # Note: Additionally `request` and `encoding` will also be added
+ # to the context by the Request object.
return {
'view': self,
'args': getattr(self, 'args', ()),
@@ -110,6 +190,34 @@ class APIView(View):
'request': getattr(self, 'request', None)
}
+ def get_exception_handler_context(self):
+ """
+ Returns a dict that is passed through to EXCEPTION_HANDLER,
+ as the `context` argument.
+ """
+ return {
+ 'view': self,
+ 'args': getattr(self, 'args', ()),
+ 'kwargs': getattr(self, 'kwargs', {}),
+ 'request': getattr(self, 'request', None)
+ }
+
+ def get_view_name(self):
+ """
+ Return the view name, as used in OPTIONS responses and in the
+ browsable API.
+ """
+ func = self.settings.VIEW_NAME_FUNCTION
+ return func(self.__class__, getattr(self, 'suffix', None))
+
+ def get_view_description(self, html=False):
+ """
+ Return some descriptive text for the view, as used in OPTIONS responses
+ and in the browsable API.
+ """
+ func = self.settings.VIEW_DESCRIPTION_FUNCTION
+ return func(self.__class__, html)
+
# API policy instantiation methods
def get_format_suffix(self, **kwargs):
@@ -210,19 +318,31 @@ class APIView(View):
if not throttle.allow_request(request, self):
self.throttled(request, throttle.wait())
+ def determine_version(self, request, *args, **kwargs):
+ """
+ If versioning is being used, then determine any API version for the
+ incoming request. Returns a two-tuple of (version, versioning_scheme)
+ """
+ if self.versioning_class is None:
+ return (None, None)
+ scheme = self.versioning_class()
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
# Dispatch methods
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Returns the initial request object.
"""
parser_context = self.get_parser_context(request)
- return Request(request,
- parsers=self.get_parsers(),
- authenticators=self.get_authenticators(),
- negotiator=self.get_content_negotiator(),
- parser_context=parser_context)
+ return Request(
+ request,
+ parsers=self.get_parsers(),
+ authenticators=self.get_authenticators(),
+ negotiator=self.get_content_negotiator(),
+ parser_context=parser_context
+ )
def initial(self, request, *args, **kwargs):
"""
@@ -239,6 +359,10 @@ class APIView(View):
neg = self.perform_content_negotiation(request)
request.accepted_renderer, request.accepted_media_type = neg
+ # Determine the API version, if versioning is in use.
+ version, scheme = self.determine_version(request, *args, **kwargs)
+ request.version, request.versioning_scheme = version, scheme
+
def finalize_response(self, request, response, *args, **kwargs):
"""
Returns the final response object.
@@ -269,37 +393,38 @@ class APIView(View):
Handle any exception that occurs, by returning an appropriate response,
or re-raising the error.
"""
- if isinstance(exc, exceptions.Throttled):
- # Throttle wait header
- self.headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait
-
if isinstance(exc, (exceptions.NotAuthenticated,
exceptions.AuthenticationFailed)):
# WWW-Authenticate header for 401 responses, else coerce to 403
auth_header = self.get_authenticate_header(self.request)
if auth_header:
- self.headers['WWW-Authenticate'] = auth_header
+ exc.auth_header = auth_header
else:
exc.status_code = status.HTTP_403_FORBIDDEN
- if isinstance(exc, exceptions.APIException):
- return Response({'detail': exc.detail},
- status=exc.status_code,
- exception=True)
- elif isinstance(exc, Http404):
- return Response({'detail': 'Not found'},
- status=status.HTTP_404_NOT_FOUND,
- exception=True)
- elif isinstance(exc, PermissionDenied):
- return Response({'detail': 'Permission denied'},
- status=status.HTTP_403_FORBIDDEN,
- exception=True)
- raise
-
- # Note: session based authentication is explicitly CSRF validated,
- # all other authentication is CSRF exempt.
- @csrf_exempt
+ exception_handler = self.settings.EXCEPTION_HANDLER
+
+ if len(inspect.getargspec(exception_handler).args) == 1:
+ warnings.warn(
+ 'The `exception_handler(exc)` call signature is deprecated. '
+ 'Use `exception_handler(exc, context) instead.',
+ DeprecationWarning
+ )
+ response = exception_handler(exc)
+ else:
+ context = self.get_exception_handler_context()
+ response = exception_handler(exc, context)
+
+ if response is None:
+ raise
+
+ response.exception = True
+ return response
+
+ # Note: Views are made CSRF exempt from within `as_view` as to prevent
+ # accidental removal of this exemption in cases where `dispatch` needs to
+ # be overridden.
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
@@ -332,26 +457,8 @@ class APIView(View):
def options(self, request, *args, **kwargs):
"""
Handler method for HTTP 'OPTIONS' request.
- We may as well implement this as Django will otherwise provide
- a less useful default implementation.
- """
- return Response(self.metadata(request), status=status.HTTP_200_OK)
-
- def metadata(self, request):
"""
- Return a dictionary of metadata about the view.
- Used to return responses for OPTIONS requests.
- """
-
- # This is used by ViewSets to disambiguate instance vs list views
- view_name_suffix = getattr(self, 'suffix', None)
-
- # By default we can't provide any form-like information, however the
- # generic views override this implementation and add additional
- # information for POST and PUT methods, based on the serializer.
- ret = SortedDict()
- ret['name'] = get_view_name(self.__class__, view_name_suffix)
- ret['description'] = get_view_description(self.__class__)
- ret['renders'] = [renderer.media_type for renderer in self.renderer_classes]
- ret['parses'] = [parser.media_type for parser in self.parser_classes]
- return ret
+ if self.metadata_class is None:
+ return self.http_method_not_allowed(request, *args, **kwargs)
+ data = self.metadata_class().determine_metadata(request, self)
+ return Response(data, status=status.HTTP_200_OK)
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index d91323f2..88c763da 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views.
user_detail = UserViewSet.as_view({'get': 'retrieve'})
Typically, rather than instantiate views from viewsets directly, you'll
-regsiter the viewset with a router and let the URL conf be determined
+register the viewset with a router and let the URL conf be determined
automatically.
router = DefaultRouter()
@@ -20,6 +20,7 @@ from __future__ import unicode_literals
from functools import update_wrapper
from django.utils.decorators import classonlymethod
+from django.views.decorators.csrf import csrf_exempt
from rest_framework import views, generics, mixins
@@ -43,10 +44,16 @@ class ViewSetMixin(object):
instantiated view, we need to totally reimplement `.as_view`,
and slightly modify the view function that is created and returned.
"""
- # The suffix initkwarg is reserved for identifing the viewset type
+ # The suffix initkwarg is reserved for identifying the viewset type
# eg. 'List' or 'Instance'.
cls.suffix = None
+ # actions must not be empty
+ if not actions:
+ raise TypeError("The `actions` argument must be provided when "
+ "calling `.as_view()` on a ViewSet. For example "
+ "`.as_view({'get': 'list'})`")
+
# sanitize keyword arguments
for key in initkwargs:
if key in cls.http_method_names:
@@ -89,14 +96,14 @@ class ViewSetMixin(object):
# resolved URL.
view.cls = cls
view.suffix = initkwargs.get('suffix', None)
- return view
+ return csrf_exempt(view)
- def initialize_request(self, request, *args, **kargs):
+ def initialize_request(self, request, *args, **kwargs):
"""
Set the `.action` attribute on the view,
depending on the request method.
"""
- request = super(ViewSetMixin, self).initialize_request(request, *args, **kargs)
+ request = super(ViewSetMixin, self).initialize_request(request, *args, **kwargs)
self.action = self.action_map.get(request.method.lower())
return request
@@ -127,11 +134,11 @@ class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
class ModelViewSet(mixins.CreateModelMixin,
- mixins.RetrieveModelMixin,
- mixins.UpdateModelMixin,
- mixins.DestroyModelMixin,
- mixins.ListModelMixin,
- GenericViewSet):
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
diff --git a/runtests.py b/runtests.py
new file mode 100755
index 00000000..0008bfae
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,91 @@
+#! /usr/bin/env python
+from __future__ import print_function
+
+import pytest
+import sys
+import os
+import subprocess
+
+
+PYTEST_ARGS = {
+ 'default': ['tests', '--tb=short'],
+ 'fast': ['tests', '--tb=short', '-q'],
+}
+
+FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
+
+
+sys.path.append(os.path.dirname(__file__))
+
+
+def exit_on_failure(ret, message=None):
+ if ret:
+ sys.exit(ret)
+
+
+def flake8_main(args):
+ print('Running flake8 code linting')
+ ret = subprocess.call(['flake8'] + args)
+ print('flake8 failed' if ret else 'flake8 passed')
+ return ret
+
+
+def split_class_and_function(string):
+ class_string, function_string = string.split('.', 1)
+ return "%s and %s" % (class_string, function_string)
+
+
+def is_function(string):
+ # `True` if it looks like a test function is included in the string.
+ return string.startswith('test_') or '.test_' in string
+
+
+def is_class(string):
+ # `True` if first character is uppercase - assume it's a class name.
+ return string[0] == string[0].upper()
+
+
+if __name__ == "__main__":
+ try:
+ sys.argv.remove('--nolint')
+ except ValueError:
+ run_flake8 = True
+ else:
+ run_flake8 = False
+
+ try:
+ sys.argv.remove('--lintonly')
+ except ValueError:
+ run_tests = True
+ else:
+ run_tests = False
+
+ try:
+ sys.argv.remove('--fast')
+ except ValueError:
+ style = 'default'
+ else:
+ style = 'fast'
+ run_flake8 = False
+
+ if len(sys.argv) > 1:
+ pytest_args = sys.argv[1:]
+ first_arg = pytest_args[0]
+ if first_arg.startswith('-'):
+ # `runtests.py [flags]`
+ pytest_args = ['tests'] + pytest_args
+ elif is_class(first_arg) and is_function(first_arg):
+ # `runtests.py TestCase.test_function [flags]`
+ expression = split_class_and_function(first_arg)
+ pytest_args = ['tests', '-k', expression] + pytest_args[1:]
+ elif is_class(first_arg) or is_function(first_arg):
+ # `runtests.py TestCase [flags]`
+ # `runtests.py test_function [flags]`
+ pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:]
+ else:
+ pytest_args = PYTEST_ARGS[style]
+
+ if run_tests:
+ exit_on_failure(pytest.main(pytest_args))
+ if run_flake8:
+ exit_on_failure(flake8_main(FLAKE8_ARGS))
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 00000000..5e409001
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[wheel]
+universal = 1
diff --git a/setup.py b/setup.py
index adf083cb..4cdcfa86 100755
--- a/setup.py
+++ b/setup.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
from setuptools import setup
+from setuptools.command.test import test as TestCommand
import re
import os
import sys
@@ -12,7 +13,7 @@ def get_version(package):
Return package version as listed in `__version__` in `init.py`.
"""
init_py = open(os.path.join(package, '__init__.py')).read()
- return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
+ return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
def get_packages(package):
@@ -44,7 +45,14 @@ version = get_version('rest_framework')
if sys.argv[-1] == 'publish':
- os.system("python setup.py sdist upload")
+ if os.system("pip freeze | grep wheel"):
+ print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
+ sys.exit()
+ if os.system("pip freeze | grep twine"):
+ print("twine not installed.\nUse `pip install twine`.\nExiting.")
+ sys.exit()
+ os.system("python setup.py sdist bdist_wheel")
+ os.system("twine upload dist/*")
print("You probably want to also tag the version now:")
print(" git tag -a %s -m 'version %s'" % (version, version))
print(" git push --tags")
@@ -54,15 +62,15 @@ if sys.argv[-1] == 'publish':
setup(
name='djangorestframework',
version=version,
- url='http://django-rest-framework.org',
+ url='http://www.django-rest-framework.org',
license='BSD',
description='Web APIs for Django, made easy.',
author='Tom Christie',
author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
packages=get_packages('rest_framework'),
package_data=get_package_data('rest_framework'),
- test_suite='rest_framework.runtests.runtests.main',
install_requires=[],
+ zip_safe=False,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Web Environment',
diff --git a/rest_framework/tests/__init__.py b/tests/__init__.py
index e69de29b..e69de29b 100644
--- a/rest_framework/tests/__init__.py
+++ b/tests/__init__.py
diff --git a/rest_framework/tests/extras/__init__.py b/tests/browsable_api/__init__.py
index e69de29b..e69de29b 100644
--- a/rest_framework/tests/extras/__init__.py
+++ b/tests/browsable_api/__init__.py
diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py
new file mode 100644
index 00000000..97bc1036
--- /dev/null
+++ b/tests/browsable_api/auth_urls.py
@@ -0,0 +1,11 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+
+from .views import MockView
+
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+)
diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py
new file mode 100644
index 00000000..5e3604a6
--- /dev/null
+++ b/tests/browsable_api/no_auth_urls.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+)
diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py
new file mode 100644
index 00000000..5f264783
--- /dev/null
+++ b/tests/browsable_api/test_browsable_api.py
@@ -0,0 +1,65 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from rest_framework.test import APIClient
+
+
+class DropdownWithAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views enabled."""
+
+ urls = 'tests.browsable_api.auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_logout_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, '>Log out<')
+
+ def test_login_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertContains(response, '>Log in<')
+
+
+class NoDropdownWithoutAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views NOT enabled."""
+
+ urls = 'tests.browsable_api.no_auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_dropdown_not_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
+
+ def test_dropdown_not_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py
new file mode 100644
index 00000000..000f4e80
--- /dev/null
+++ b/tests/browsable_api/views.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from rest_framework.views import APIView
+from rest_framework import authentication
+from rest_framework import renderers
+from rest_framework.response import Response
+
+
+class MockView(APIView):
+
+ authentication_classes = (authentication.SessionAuthentication,)
+ renderer_classes = (renderers.BrowsableAPIRenderer,)
+
+ def get(self, request):
+ return Response({'a': 1, 'b': 2, 'c': 3})
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..44ed070b
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,66 @@
+def pytest_configure():
+ from django.conf import settings
+
+ settings.configure(
+ DEBUG_PROPAGATE_EXCEPTIONS=True,
+ DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:'}},
+ SITE_ID=1,
+ SECRET_KEY='not very secret in tests',
+ USE_I18N=True,
+ USE_L10N=True,
+ STATIC_URL='/static/',
+ ROOT_URLCONF='tests.urls',
+ TEMPLATE_LOADERS=(
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ ),
+ MIDDLEWARE_CLASSES=(
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ ),
+ INSTALLED_APPS=(
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+
+ 'rest_framework',
+ 'rest_framework.authtoken',
+ 'tests',
+ ),
+ PASSWORD_HASHERS=(
+ 'django.contrib.auth.hashers.SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2PasswordHasher',
+ 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
+ 'django.contrib.auth.hashers.BCryptPasswordHasher',
+ 'django.contrib.auth.hashers.MD5PasswordHasher',
+ 'django.contrib.auth.hashers.CryptPasswordHasher',
+ ),
+ )
+
+ # guardian is optional
+ try:
+ import guardian # NOQA
+ except ImportError:
+ pass
+ else:
+ settings.ANONYMOUS_USER_ID = -1
+ settings.AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.ModelBackend',
+ 'guardian.backends.ObjectPermissionBackend',
+ )
+ settings.INSTALLED_APPS += (
+ 'guardian',
+ )
+
+ try:
+ import django
+ django.setup()
+ except AttributeError:
+ pass
diff --git a/rest_framework/tests/description.py b/tests/description.py
index b46d7f54..b46d7f54 100644
--- a/rest_framework/tests/description.py
+++ b/tests/description.py
diff --git a/tests/models.py b/tests/models.py
new file mode 100644
index 00000000..456b0a0b
--- /dev/null
+++ b/tests/models.py
@@ -0,0 +1,70 @@
+from __future__ import unicode_literals
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+
+class RESTFrameworkModel(models.Model):
+ """
+ Base for test models that sets app_label, so they play nicely.
+ """
+
+ class Meta:
+ app_label = 'tests'
+ abstract = True
+
+
+class BasicModel(RESTFrameworkModel):
+ text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
+
+
+class BaseFilterableItem(RESTFrameworkModel):
+ text = models.CharField(max_length=100)
+
+ class Meta:
+ abstract = True
+
+
+class FilterableItem(BaseFilterableItem):
+ decimal = models.DecimalField(max_digits=4, decimal_places=2)
+ date = models.DateField()
+
+
+# Models for relations tests
+# ManyToMany
+class ManyToManyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
+
+
+# ForeignKey
+class ForeignKeyTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class ForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources',
+ help_text='Target', verbose_name='Target')
+
+
+# Nullable ForeignKey
+class NullableForeignKeySource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
+ related_name='nullable_sources',
+ verbose_name='Optional target object')
+
+
+# OneToOne
+class OneToOneTarget(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+
+
+class NullableOneToOneSource(RESTFrameworkModel):
+ name = models.CharField(max_length=100)
+ target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
+ related_name='nullable_source')
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
new file mode 100644
index 00000000..91e49f9d
--- /dev/null
+++ b/tests/test_authentication.py
@@ -0,0 +1,288 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+from django.contrib.auth.models import User
+from django.http import HttpResponse
+from django.test import TestCase
+from django.utils import six
+from rest_framework import HTTP_HEADER_ENCODING
+from rest_framework import exceptions
+from rest_framework import permissions
+from rest_framework import renderers
+from rest_framework.response import Response
+from rest_framework import status
+from rest_framework.authentication import (
+ BaseAuthentication,
+ TokenAuthentication,
+ BasicAuthentication,
+ SessionAuthentication,
+)
+from rest_framework.authtoken.models import Token
+from rest_framework.test import APIRequestFactory, APIClient
+from rest_framework.views import APIView
+import base64
+
+factory = APIRequestFactory()
+
+
+class MockView(APIView):
+ permission_classes = (permissions.IsAuthenticated,)
+
+ def get(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+ def post(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+ def put(self, request):
+ return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+
+
+urlpatterns = patterns(
+ '',
+ (r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
+ (r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
+ (r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
+ (r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
+)
+
+
+class BasicAuthTests(TestCase):
+ """Basic authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def test_post_form_passing_basic_auth(self):
+ """Ensure POSTing json over basic auth with correct credentials passes and does not require CSRF"""
+ credentials = ('%s:%s' % (self.username, self.password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ auth = 'Basic %s' % base64_credentials
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_passing_basic_auth(self):
+ """Ensure POSTing form over basic auth with correct credentials passes and does not require CSRF"""
+ credentials = ('%s:%s' % (self.username, self.password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ auth = 'Basic %s' % base64_credentials
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_form_failing_basic_auth(self):
+ """Ensure POSTing form over basic auth without correct credentials fails"""
+ response = self.csrf_client.post('/basic/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_post_json_failing_basic_auth(self):
+ """Ensure POSTing json over basic auth without correct credentials fails"""
+ response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+ self.assertEqual(response['WWW-Authenticate'], 'Basic realm="api"')
+
+
+class SessionAuthTests(TestCase):
+ """User session authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.non_csrf_client = APIClient(enforce_csrf_checks=False)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.csrf_client.logout()
+
+ def test_login_view_renders_on_get(self):
+ """
+ Ensure the login template renders for a basic GET.
+
+ cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
+ """
+ response = self.csrf_client.get('/auth/login/')
+ self.assertContains(response, '<label for="id_username">Username:</label>')
+
+ def test_post_form_session_auth_failing_csrf(self):
+ """
+ Ensure POSTing form over session authentication without CSRF token fails.
+ """
+ self.csrf_client.login(username=self.username, password=self.password)
+ response = self.csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_post_form_session_auth_passing(self):
+ """
+ Ensure POSTing form over session authentication with logged in user and CSRF token passes.
+ """
+ self.non_csrf_client.login(username=self.username, password=self.password)
+ response = self.non_csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_put_form_session_auth_passing(self):
+ """
+ Ensure PUTting form over session authentication with logged in user and CSRF token passes.
+ """
+ self.non_csrf_client.login(username=self.username, password=self.password)
+ response = self.non_csrf_client.put('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_form_session_auth_failing(self):
+ """
+ Ensure POSTing form over session authentication without logged in user fails.
+ """
+ response = self.csrf_client.post('/session/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+
+class TokenAuthTests(TestCase):
+ """Token authentication"""
+ urls = 'tests.test_authentication'
+
+ def setUp(self):
+ self.csrf_client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ self.key = 'abcd1234'
+ self.token = Token.objects.create(key=self.key, user=self.user)
+
+ def test_post_form_passing_token_auth(self):
+ """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
+ auth = 'Token ' + self.key
+ response = self.csrf_client.post('/token/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_passing_token_auth(self):
+ """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
+ auth = "Token " + self.key
+ response = self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_post_json_makes_one_db_query(self):
+ """Ensure that authenticating a user using a token performs only one DB query"""
+ auth = "Token " + self.key
+
+ def func_to_test():
+ return self.csrf_client.post('/token/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth)
+
+ self.assertNumQueries(1, func_to_test)
+
+ def test_post_form_failing_token_auth(self):
+ """Ensure POSTing form over token auth without correct credentials fails"""
+ response = self.csrf_client.post('/token/', {'example': 'example'})
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_post_json_failing_token_auth(self):
+ """Ensure POSTing json over token auth without correct credentials fails"""
+ response = self.csrf_client.post('/token/', {'example': 'example'}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
+
+ def test_token_has_auto_assigned_key_if_none_provided(self):
+ """Ensure creating a token with no key will auto-assign a key"""
+ self.token.delete()
+ token = Token.objects.create(user=self.user)
+ self.assertTrue(bool(token.key))
+
+ def test_generate_key_returns_string(self):
+ """Ensure generate_key returns a string"""
+ token = Token()
+ key = token.generate_key()
+ self.assertTrue(isinstance(key, six.string_types))
+
+ def test_token_login_json(self):
+ """Ensure token login view using JSON POST works."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': self.password}, format='json')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['token'], self.key)
+
+ def test_token_login_json_bad_creds(self):
+ """Ensure token login view using JSON POST fails if bad credentials are used."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': "badpass"}, format='json')
+ self.assertEqual(response.status_code, 400)
+
+ def test_token_login_json_missing_fields(self):
+ """Ensure token login view using JSON POST fails if missing fields."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username}, format='json')
+ self.assertEqual(response.status_code, 400)
+
+ def test_token_login_form(self):
+ """Ensure token login view using form POST works."""
+ client = APIClient(enforce_csrf_checks=True)
+ response = client.post('/auth-token/',
+ {'username': self.username, 'password': self.password})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data['token'], self.key)
+
+
+class IncorrectCredentialsTests(TestCase):
+ def test_incorrect_credentials(self):
+ """
+ If a request contains bad authentication credentials, then
+ authentication should run and error, even if no permissions
+ are set on the view.
+ """
+ class IncorrectCredentialsAuth(BaseAuthentication):
+ def authenticate(self, request):
+ raise exceptions.AuthenticationFailed('Bad credentials')
+
+ request = factory.get('/')
+ view = MockView.as_view(
+ authentication_classes=(IncorrectCredentialsAuth,),
+ permission_classes=()
+ )
+ response = view(request)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+ self.assertEqual(response.data, {'detail': 'Bad credentials'})
+
+
+class FailingAuthAccessedInRenderer(TestCase):
+ def setUp(self):
+ class AuthAccessingRenderer(renderers.BaseRenderer):
+ media_type = 'text/plain'
+ format = 'txt'
+
+ def render(self, data, media_type=None, renderer_context=None):
+ request = renderer_context['request']
+ if request.user.is_authenticated():
+ return b'authenticated'
+ return b'not authenticated'
+
+ class FailingAuth(BaseAuthentication):
+ def authenticate(self, request):
+ raise exceptions.AuthenticationFailed('authentication failed')
+
+ class ExampleView(APIView):
+ authentication_classes = (FailingAuth,)
+ renderer_classes = (AuthAccessingRenderer,)
+
+ def get(self, request):
+ return Response({'foo': 'bar'})
+
+ self.view = ExampleView.as_view()
+
+ def test_failing_auth_accessed_in_renderer(self):
+ """
+ When authentication fails the renderer should still be able to access
+ `request.user` without raising an exception. Particularly relevant
+ to HTML responses that might reasonably access `request.user`.
+ """
+ request = factory.get('/')
+ response = self.view(request)
+ content = response.render().content
+ self.assertEqual(content, b'not authenticated')
diff --git a/tests/test_bound_fields.py b/tests/test_bound_fields.py
new file mode 100644
index 00000000..bfc54b23
--- /dev/null
+++ b/tests/test_bound_fields.py
@@ -0,0 +1,69 @@
+from rest_framework import serializers
+
+
+class TestSimpleBoundField:
+ def test_empty_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer()
+
+ assert serializer['text'].value == ''
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is None
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+ def test_populated_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer(data={'text': 'abc', 'amount': 123})
+ assert serializer.is_valid()
+ assert serializer['text'].value == 'abc'
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is 123
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+ def test_error_bound_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ serializer = ExampleSerializer(data={'text': 'x' * 1000, 'amount': 123})
+ serializer.is_valid()
+
+ assert serializer['text'].value == 'x' * 1000
+ assert serializer['text'].errors == ['Ensure this field has no more than 100 characters.']
+ assert serializer['text'].name == 'text'
+ assert serializer['amount'].value is 123
+ assert serializer['amount'].errors is None
+ assert serializer['amount'].name == 'amount'
+
+
+class TestNestedBoundField:
+ def test_nested_empty_bound_field(self):
+ class Nested(serializers.Serializer):
+ more_text = serializers.CharField(max_length=100)
+ amount = serializers.IntegerField()
+
+ class ExampleSerializer(serializers.Serializer):
+ text = serializers.CharField(max_length=100)
+ nested = Nested()
+
+ serializer = ExampleSerializer()
+
+ assert serializer['text'].value == ''
+ assert serializer['text'].errors is None
+ assert serializer['text'].name == 'text'
+ assert serializer['nested']['more_text'].value == ''
+ assert serializer['nested']['more_text'].errors is None
+ assert serializer['nested']['more_text'].name == 'nested.more_text'
+ assert serializer['nested']['amount'].value is None
+ assert serializer['nested']['amount'].errors is None
+ assert serializer['nested']['amount'].name == 'nested.amount'
diff --git a/rest_framework/tests/test_decorators.py b/tests/test_decorators.py
index 195f0ba3..195f0ba3 100644
--- a/rest_framework/tests/test_decorators.py
+++ b/tests/test_decorators.py
diff --git a/rest_framework/tests/test_description.py b/tests/test_description.py
index 8019f5ec..78ce2350 100644
--- a/rest_framework/tests/test_description.py
+++ b/tests/test_description.py
@@ -2,11 +2,11 @@
from __future__ import unicode_literals
from django.test import TestCase
-from rest_framework.compat import apply_markdown, smart_text
+from django.utils.encoding import python_2_unicode_compatible, smart_text
+from rest_framework.compat import apply_markdown
from rest_framework.views import APIView
-from rest_framework.tests.description import ViewWithNonASCIICharactersInDocstring
-from rest_framework.tests.description import UTF8_TEST_DOCSTRING
-from rest_framework.utils.formatting import get_view_name, get_view_description
+from .description import ViewWithNonASCIICharactersInDocstring
+from .description import UTF8_TEST_DOCSTRING
# We check that docstrings get nicely un-indented.
DESCRIPTION = """an example docstring
@@ -58,7 +58,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_name(MockView), 'Mock')
+ self.assertEqual(MockView().get_view_name(), 'Mock')
def test_view_description_uses_docstring(self):
"""Ensure view descriptions are based on the docstring."""
@@ -78,7 +78,7 @@ class TestViewNamesAndDescriptions(TestCase):
# hash style header #"""
- self.assertEqual(get_view_description(MockView), DESCRIPTION)
+ self.assertEqual(MockView().get_view_description(), DESCRIPTION)
def test_view_description_supports_unicode(self):
"""
@@ -86,7 +86,7 @@ class TestViewNamesAndDescriptions(TestCase):
"""
self.assertEqual(
- get_view_description(ViewWithNonASCIICharactersInDocstring),
+ ViewWithNonASCIICharactersInDocstring().get_view_description(),
smart_text(UTF8_TEST_DOCSTRING)
)
@@ -97,7 +97,29 @@ class TestViewNamesAndDescriptions(TestCase):
"""
class MockView(APIView):
pass
- self.assertEqual(get_view_description(MockView), '')
+ self.assertEqual(MockView().get_view_description(), '')
+
+ def test_view_description_can_be_promise(self):
+ """
+ Ensure a view may have a docstring that is actually a lazily evaluated
+ class that can be converted to a string.
+
+ See: https://github.com/tomchristie/django-rest-framework/issues/1708
+ """
+ # use a mock object instead of gettext_lazy to ensure that we can't end
+ # up with a test case string in our l10n catalog
+ @python_2_unicode_compatible
+ class MockLazyStr(object):
+ def __init__(self, string):
+ self.s = string
+
+ def __str__(self):
+ return self.s
+
+ class MockView(APIView):
+ __doc__ = MockLazyStr("a gettext string")
+
+ self.assertEqual(MockView().get_view_description(), 'a gettext string')
def test_markdown(self):
"""
diff --git a/tests/test_fields.py b/tests/test_fields.py
new file mode 100644
index 00000000..1aa528da
--- /dev/null
+++ b/tests/test_fields.py
@@ -0,0 +1,1212 @@
+from decimal import Decimal
+from django.utils import timezone
+from rest_framework import serializers
+import datetime
+import django
+import pytest
+import uuid
+
+
+# Tests for field keyword arguments and core functionality.
+# ---------------------------------------------------------
+
+class TestEmpty:
+ """
+ Tests for `required`, `allow_null`, `allow_blank`, `default`.
+ """
+ def test_required(self):
+ """
+ By default a field must be included in the input.
+ """
+ field = serializers.IntegerField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation()
+ assert exc_info.value.detail == ['This field is required.']
+
+ def test_not_required(self):
+ """
+ If `required=False` then a field may be omitted from the input.
+ """
+ field = serializers.IntegerField(required=False)
+ with pytest.raises(serializers.SkipField):
+ field.run_validation()
+
+ def test_disallow_null(self):
+ """
+ By default `None` is not a valid input.
+ """
+ field = serializers.IntegerField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation(None)
+ assert exc_info.value.detail == ['This field may not be null.']
+
+ def test_allow_null(self):
+ """
+ If `allow_null=True` then `None` is a valid input.
+ """
+ field = serializers.IntegerField(allow_null=True)
+ output = field.run_validation(None)
+ assert output is None
+
+ def test_disallow_blank(self):
+ """
+ By default '' is not a valid input.
+ """
+ field = serializers.CharField()
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ field.run_validation('')
+ assert exc_info.value.detail == ['This field may not be blank.']
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = serializers.CharField(allow_blank=True)
+ output = field.run_validation('')
+ assert output == ''
+
+ def test_default(self):
+ """
+ If `default` is set, then omitted values get the default input.
+ """
+ field = serializers.IntegerField(default=123)
+ output = field.run_validation()
+ assert output is 123
+
+
+class TestSource:
+ def test_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='other')
+ serializer = ExampleSerializer(data={'example_field': 'abc'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'other': 'abc'}
+
+ def test_redundant_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_field')
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer().fields
+ assert str(exc_info.value) == (
+ "It is redundant to specify `source='example_field'` on field "
+ "'CharField' in serializer 'ExampleSerializer', because it is the "
+ "same as the field name. Remove the `source` keyword argument."
+ )
+
+ def test_callable_source(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_callable')
+
+ class ExampleInstance(object):
+ def example_callable(self):
+ return 'example callable value'
+
+ serializer = ExampleSerializer(ExampleInstance())
+ assert serializer.data['example_field'] == 'example callable value'
+
+ def test_callable_source_raises(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.CharField(source='example_callable', read_only=True)
+
+ class ExampleInstance(object):
+ def example_callable(self):
+ raise AttributeError('method call failed')
+
+ with pytest.raises(ValueError) as exc_info:
+ serializer = ExampleSerializer(ExampleInstance())
+ serializer.data.items()
+
+ assert 'method call failed' in str(exc_info.value)
+
+
+class TestReadOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ read_only = serializers.ReadOnlyField()
+ writable = serializers.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_read_only(self):
+ """
+ Read-only serializers.should not be included in validation.
+ """
+ data = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'writable': 456}
+
+ def test_serialize_read_only(self):
+ """
+ Read-only serializers.should be serialized.
+ """
+ instance = {'read_only': 123, 'writable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'read_only': 123, 'writable': 456}
+
+
+class TestWriteOnly:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ write_only = serializers.IntegerField(write_only=True)
+ readable = serializers.IntegerField()
+ self.Serializer = TestSerializer
+
+ def test_validate_write_only(self):
+ """
+ Write-only serializers.should be included in validation.
+ """
+ data = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'write_only': 123, 'readable': 456}
+
+ def test_serialize_write_only(self):
+ """
+ Write-only serializers.should not be serialized.
+ """
+ instance = {'write_only': 123, 'readable': 456}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'readable': 456}
+
+
+class TestInitial:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ initial_field = serializers.IntegerField(initial=123)
+ blank_field = serializers.IntegerField()
+ self.serializer = TestSerializer()
+
+ def test_initial(self):
+ """
+ Initial values should be included when serializing a new representation.
+ """
+ assert self.serializer.data == {
+ 'initial_field': 123,
+ 'blank_field': None
+ }
+
+
+class TestLabel:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ labeled = serializers.IntegerField(label='My label')
+ self.serializer = TestSerializer()
+
+ def test_label(self):
+ """
+ A field's label may be set with the `label` argument.
+ """
+ fields = self.serializer.fields
+ assert fields['labeled'].label == 'My label'
+
+
+class TestInvalidErrorKey:
+ def setup(self):
+ class ExampleField(serializers.Field):
+ def to_native(self, data):
+ self.fail('incorrect')
+ self.field = ExampleField()
+
+ def test_invalid_error_key(self):
+ """
+ If a field raises a validation error, but does not have a corresponding
+ error message, then raise an appropriate assertion error.
+ """
+ with pytest.raises(AssertionError) as exc_info:
+ self.field.to_native(123)
+ expected = (
+ 'ValidationError raised by `ExampleField`, but error key '
+ '`incorrect` does not exist in the `error_messages` dictionary.'
+ )
+ assert str(exc_info.value) == expected
+
+
+class TestBooleanHTMLInput:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ archived = serializers.BooleanField()
+ self.Serializer = TestSerializer
+
+ def test_empty_html_checkbox(self):
+ """
+ HTML checkboxes do not send any value, but should be treated
+ as `False` by BooleanField.
+ """
+ # This class mocks up a dictionary like object, that behaves
+ # as if it was returned for multipart or urlencoded data.
+ class MockHTMLDict(dict):
+ getlist = None
+ serializer = self.Serializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'archived': False}
+
+
+class MockHTMLDict(dict):
+ """
+ This class mocks up a dictionary like object, that behaves
+ as if it was returned for multipart or urlencoded data.
+ """
+ getlist = None
+
+
+class TestHTMLInput:
+ def test_empty_html_charfield(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(default='happy')
+
+ serializer = TestSerializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': 'happy'}
+
+ def test_empty_html_charfield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': None}
+
+ def test_empty_html_datefield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ expiry = serializers.DateField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'expiry': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'expiry': None}
+
+ def test_empty_html_charfield_allow_null_allow_blank(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(allow_null=True, allow_blank=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'message': ''}
+
+ def test_empty_html_charfield_required_false(self):
+ class TestSerializer(serializers.Serializer):
+ message = serializers.CharField(required=False)
+
+ serializer = TestSerializer(data=MockHTMLDict())
+ assert serializer.is_valid()
+ assert serializer.validated_data == {}
+
+
+class TestCreateOnlyDefault:
+ def setup(self):
+ default = serializers.CreateOnlyDefault('2001-01-01')
+
+ class TestSerializer(serializers.Serializer):
+ published = serializers.HiddenField(default=default)
+ text = serializers.CharField()
+ self.Serializer = TestSerializer
+
+ def test_create_only_default_is_provided(self):
+ serializer = self.Serializer(data={'text': 'example'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'text': 'example', 'published': '2001-01-01'
+ }
+
+ def test_create_only_default_is_not_provided_on_update(self):
+ instance = {
+ 'text': 'example', 'published': '2001-01-01'
+ }
+ serializer = self.Serializer(instance, data={'text': 'example'})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'text': 'example',
+ }
+
+ def test_create_only_default_callable_sets_context(self):
+ """
+ CreateOnlyDefault instances with a callable default should set_context
+ on the callable if possible
+ """
+ class TestCallableDefault:
+ def set_context(self, serializer_field):
+ self.field = serializer_field
+
+ def __call__(self):
+ return "success" if hasattr(self, 'field') else "failure"
+
+ class TestSerializer(serializers.Serializer):
+ context_set = serializers.CharField(default=serializers.CreateOnlyDefault(TestCallableDefault()))
+
+ serializer = TestSerializer(data={})
+ assert serializer.is_valid()
+ assert serializer.validated_data['context_set'] == 'success'
+
+
+# Tests for field input and output values.
+# ----------------------------------------
+
+def get_items(mapping_or_list_of_two_tuples):
+ # Tests accept either lists of two tuples, or dictionaries.
+ if isinstance(mapping_or_list_of_two_tuples, dict):
+ # {value: expected}
+ return mapping_or_list_of_two_tuples.items()
+ # [(value, expected), ...]
+ return mapping_or_list_of_two_tuples
+
+
+class FieldValues:
+ """
+ Base class for testing valid and invalid input values.
+ """
+ def test_valid_inputs(self):
+ """
+ Ensure that valid values return the expected validated data.
+ """
+ for input_value, expected_output in get_items(self.valid_inputs):
+ assert self.field.run_validation(input_value) == expected_output
+
+ def test_invalid_inputs(self):
+ """
+ Ensure that invalid values raise the expected validation error.
+ """
+ for input_value, expected_failure in get_items(self.invalid_inputs):
+ with pytest.raises(serializers.ValidationError) as exc_info:
+ self.field.run_validation(input_value)
+ assert exc_info.value.detail == expected_failure
+
+ def test_outputs(self):
+ for output_value, expected_output in get_items(self.outputs):
+ assert self.field.to_representation(output_value) == expected_output
+
+
+# Boolean types...
+
+class TestBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ }
+ invalid_inputs = {
+ 'foo': ['"foo" is not a valid boolean.'],
+ None: ['This field may not be null.']
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ '1': True,
+ '0': False,
+ 1: True,
+ 0: False,
+ True: True,
+ False: False,
+ 'other': True
+ }
+ field = serializers.BooleanField()
+
+
+class TestNullBooleanField(FieldValues):
+ """
+ Valid and invalid values for `BooleanField`.
+ """
+ valid_inputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None
+ }
+ invalid_inputs = {
+ 'foo': ['"foo" is not a valid boolean.'],
+ }
+ outputs = {
+ 'true': True,
+ 'false': False,
+ 'null': None,
+ True: True,
+ False: False,
+ None: None,
+ 'other': True
+ }
+ field = serializers.NullBooleanField()
+
+
+# String types...
+
+class TestCharField(FieldValues):
+ """
+ Valid and invalid values for `CharField`.
+ """
+ valid_inputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ invalid_inputs = {
+ '': ['This field may not be blank.']
+ }
+ outputs = {
+ 1: '1',
+ 'abc': 'abc'
+ }
+ field = serializers.CharField()
+
+ def test_trim_whitespace_default(self):
+ field = serializers.CharField()
+ assert field.to_internal_value(' abc ') == 'abc'
+
+ def test_trim_whitespace_disabled(self):
+ field = serializers.CharField(trim_whitespace=False)
+ assert field.to_internal_value(' abc ') == ' abc '
+
+
+class TestEmailField(FieldValues):
+ """
+ Valid and invalid values for `EmailField`.
+ """
+ valid_inputs = {
+ 'example@example.com': 'example@example.com',
+ ' example@example.com ': 'example@example.com',
+ }
+ invalid_inputs = {
+ 'examplecom': ['Enter a valid email address.']
+ }
+ outputs = {}
+ field = serializers.EmailField()
+
+
+class TestRegexField(FieldValues):
+ """
+ Valid and invalid values for `RegexField`.
+ """
+ valid_inputs = {
+ 'a9': 'a9',
+ }
+ invalid_inputs = {
+ 'A9': ["This value does not match the required pattern."]
+ }
+ outputs = {}
+ field = serializers.RegexField(regex='[a-z][0-9]')
+
+
+class TestSlugField(FieldValues):
+ """
+ Valid and invalid values for `SlugField`.
+ """
+ valid_inputs = {
+ 'slug-99': 'slug-99',
+ }
+ invalid_inputs = {
+ 'slug 99': ['Enter a valid "slug" consisting of letters, numbers, underscores or hyphens.']
+ }
+ outputs = {}
+ field = serializers.SlugField()
+
+
+class TestURLField(FieldValues):
+ """
+ Valid and invalid values for `URLField`.
+ """
+ valid_inputs = {
+ 'http://example.com': 'http://example.com',
+ }
+ invalid_inputs = {
+ 'example.com': ['Enter a valid URL.']
+ }
+ outputs = {}
+ field = serializers.URLField()
+
+
+class TestUUIDField(FieldValues):
+ """
+ Valid and invalid values for `UUIDField`.
+ """
+ valid_inputs = {
+ '825d7aeb-05a9-45b5-a5b7-05df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'),
+ '825d7aeb05a945b5a5b705df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda')
+ }
+ invalid_inputs = {
+ '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.']
+ }
+ outputs = {
+ uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'): '825d7aeb-05a9-45b5-a5b7-05df87923cda'
+ }
+ field = serializers.UUIDField()
+
+
+# Number types...
+
+class TestIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField`.
+ """
+ valid_inputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ invalid_inputs = {
+ 'abc': ['A valid integer is required.']
+ }
+ outputs = {
+ '1': 1,
+ '0': 0,
+ 1: 1,
+ 0: 0,
+ 1.0: 1,
+ 0.0: 0
+ }
+ field = serializers.IntegerField()
+
+
+class TestMinMaxIntegerField(FieldValues):
+ """
+ Valid and invalid values for `IntegerField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 0: ['Ensure this value is greater than or equal to 1.'],
+ 4: ['Ensure this value is less than or equal to 3.'],
+ '0': ['Ensure this value is greater than or equal to 1.'],
+ '4': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = serializers.IntegerField(min_value=1, max_value=3)
+
+
+class TestFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField`.
+ """
+ valid_inputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ invalid_inputs = {
+ 'abc': ["A valid number is required."]
+ }
+ outputs = {
+ '1': 1.0,
+ '0': 0.0,
+ 1: 1.0,
+ 0: 0.0,
+ 1.0: 1.0,
+ 0.0: 0.0,
+ }
+ field = serializers.FloatField()
+
+
+class TestMinMaxFloatField(FieldValues):
+ """
+ Valid and invalid values for `FloatField` with min and max limits.
+ """
+ valid_inputs = {
+ '1': 1,
+ '3': 3,
+ 1: 1,
+ 3: 3,
+ 1.0: 1.0,
+ 3.0: 3.0,
+ }
+ invalid_inputs = {
+ 0.9: ['Ensure this value is greater than or equal to 1.'],
+ 3.1: ['Ensure this value is less than or equal to 3.'],
+ '0.0': ['Ensure this value is greater than or equal to 1.'],
+ '3.1': ['Ensure this value is less than or equal to 3.'],
+ }
+ outputs = {}
+ field = serializers.FloatField(min_value=1, max_value=3)
+
+
+class TestDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField`.
+ """
+ valid_inputs = {
+ '12.3': Decimal('12.3'),
+ '0.1': Decimal('0.1'),
+ 10: Decimal('10'),
+ 0: Decimal('0'),
+ 12.3: Decimal('12.3'),
+ 0.1: Decimal('0.1'),
+ }
+ invalid_inputs = (
+ ('abc', ["A valid number is required."]),
+ (Decimal('Nan'), ["A valid number is required."]),
+ (Decimal('Inf'), ["A valid number is required."]),
+ ('12.345', ["Ensure that there are no more than 3 digits in total."]),
+ ('0.01', ["Ensure that there are no more than 1 decimal places."]),
+ (123, ["Ensure that there are no more than 2 digits before the decimal point."])
+ )
+ outputs = {
+ '1': '1.0',
+ '0': '0.0',
+ '1.09': '1.1',
+ '0.04': '0.0',
+ 1: '1.0',
+ 0: '0.0',
+ Decimal('1.0'): '1.0',
+ Decimal('0.0'): '0.0',
+ Decimal('1.09'): '1.1',
+ Decimal('0.04'): '0.0'
+ }
+ field = serializers.DecimalField(max_digits=3, decimal_places=1)
+
+
+class TestMinMaxDecimalField(FieldValues):
+ """
+ Valid and invalid values for `DecimalField` with min and max limits.
+ """
+ valid_inputs = {
+ '10.0': Decimal('10.0'),
+ '20.0': Decimal('20.0'),
+ }
+ invalid_inputs = {
+ '9.9': ['Ensure this value is greater than or equal to 10.'],
+ '20.1': ['Ensure this value is less than or equal to 20.'],
+ }
+ outputs = {}
+ field = serializers.DecimalField(
+ max_digits=3, decimal_places=1,
+ min_value=10, max_value=20
+ )
+
+
+class TestNoStringCoercionDecimalField(FieldValues):
+ """
+ Output values for `DecimalField` with `coerce_to_string=False`.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ 1.09: Decimal('1.1'),
+ 0.04: Decimal('0.0'),
+ '1.09': Decimal('1.1'),
+ '0.04': Decimal('0.0'),
+ Decimal('1.09'): Decimal('1.1'),
+ Decimal('0.04'): Decimal('0.0'),
+ }
+ field = serializers.DecimalField(
+ max_digits=3, decimal_places=1,
+ coerce_to_string=False
+ )
+
+
+# Date & time serializers...
+
+class TestDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField`.
+ """
+ valid_inputs = {
+ '2001-01-01': datetime.date(2001, 1, 1),
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ 'abc': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
+ '2001-99-99': ['Date has wrong format. Use one of these formats instead: YYYY[-MM[-DD]].'],
+ datetime.datetime(2001, 1, 1, 12, 00): ['Expected a date but got a datetime.'],
+ }
+ outputs = {
+ datetime.date(2001, 1, 1): '2001-01-01'
+ }
+ field = serializers.DateField()
+
+
+class TestCustomInputFormatDateField(FieldValues):
+ """
+ Valid and invalid values for `DateField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1 Jan 2001': datetime.date(2001, 1, 1),
+ }
+ invalid_inputs = {
+ '2001-01-01': ['Date has wrong format. Use one of these formats instead: DD [Jan-Dec] YYYY.']
+ }
+ outputs = {}
+ field = serializers.DateField(input_formats=['%d %b %Y'])
+
+
+class TestCustomOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): '01 Jan 2001'
+ }
+ field = serializers.DateField(format='%d %b %Y')
+
+
+class TestNoOutputFormatDateField(FieldValues):
+ """
+ Values for `DateField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.date(2001, 1, 1): datetime.date(2001, 1, 1)
+ }
+ field = serializers.DateField(format=None)
+
+
+class TestDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField`.
+ """
+ valid_inputs = {
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()),
+ # Django 1.4 does not support timezone string parsing.
+ '2001-01-01T14:00+01:00' if (django.VERSION > (1, 4)) else '2001-01-01T13:00Z': datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC())
+ }
+ invalid_inputs = {
+ 'abc': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
+ '2001-99-99T99:00': ['Datetime has wrong format. Use one of these formats instead: YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z].'],
+ datetime.date(2001, 1, 1): ['Expected a datetime but got a date.'],
+ }
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '2001-01-01T13:00:00',
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): '2001-01-01T13:00:00Z'
+ }
+ field = serializers.DateTimeField(default_timezone=timezone.UTC())
+
+
+class TestCustomInputFormatDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with a cutom input format.
+ """
+ valid_inputs = {
+ '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()),
+ }
+ invalid_inputs = {
+ '2001-01-01T20:50': ['Datetime has wrong format. Use one of these formats instead: hh:mm[AM|PM], DD [Jan-Dec] YYYY.']
+ }
+ outputs = {}
+ field = serializers.DateTimeField(default_timezone=timezone.UTC(), input_formats=['%I:%M%p, %d %b %Y'])
+
+
+class TestCustomOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): '01:00PM, 01 Jan 2001',
+ }
+ field = serializers.DateTimeField(format='%I:%M%p, %d %b %Y')
+
+
+class TestNoOutputFormatDateTimeField(FieldValues):
+ """
+ Values for `DateTimeField` with no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.datetime(2001, 1, 1, 13, 00): datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ field = serializers.DateTimeField(format=None)
+
+
+class TestNaiveDateTimeField(FieldValues):
+ """
+ Valid and invalid values for `DateTimeField` with naive datetimes.
+ """
+ valid_inputs = {
+ datetime.datetime(2001, 1, 1, 13, 00, tzinfo=timezone.UTC()): datetime.datetime(2001, 1, 1, 13, 00),
+ '2001-01-01 13:00': datetime.datetime(2001, 1, 1, 13, 00),
+ }
+ invalid_inputs = {}
+ outputs = {}
+ field = serializers.DateTimeField(default_timezone=None)
+
+
+class TestTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField`.
+ """
+ valid_inputs = {
+ '13:00': datetime.time(13, 00),
+ datetime.time(13, 00): datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ 'abc': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
+ '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'],
+ }
+ outputs = {
+ datetime.time(13, 00): '13:00:00'
+ }
+ field = serializers.TimeField()
+
+
+class TestCustomInputFormatTimeField(FieldValues):
+ """
+ Valid and invalid values for `TimeField` with a custom input format.
+ """
+ valid_inputs = {
+ '1:00pm': datetime.time(13, 00),
+ }
+ invalid_inputs = {
+ '13:00': ['Time has wrong format. Use one of these formats instead: hh:mm[AM|PM].'],
+ }
+ outputs = {}
+ field = serializers.TimeField(input_formats=['%I:%M%p'])
+
+
+class TestCustomOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a custom output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): '01:00PM'
+ }
+ field = serializers.TimeField(format='%I:%M%p')
+
+
+class TestNoOutputFormatTimeField(FieldValues):
+ """
+ Values for `TimeField` with a no output format.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = {
+ datetime.time(13, 00): datetime.time(13, 00)
+ }
+ field = serializers.TimeField(format=None)
+
+
+# Choice types...
+
+class TestChoiceField(FieldValues):
+ """
+ Valid and invalid values for `ChoiceField`.
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'amazing': ['"amazing" is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good',
+ '': ''
+ }
+ field = serializers.ChoiceField(
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+
+ def test_allow_blank(self):
+ """
+ If `allow_blank=True` then '' is a valid input.
+ """
+ field = serializers.ChoiceField(
+ allow_blank=True,
+ choices=[
+ ('poor', 'Poor quality'),
+ ('medium', 'Medium quality'),
+ ('good', 'Good quality'),
+ ]
+ )
+ output = field.run_validation('')
+ assert output == ''
+
+
+class TestChoiceFieldWithType(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses an integer type,
+ instead of a char type.
+ """
+ valid_inputs = {
+ '1': 1,
+ 3: 3,
+ }
+ invalid_inputs = {
+ 5: ['"5" is not a valid choice.'],
+ 'abc': ['"abc" is not a valid choice.']
+ }
+ outputs = {
+ '1': 1,
+ 1: 1
+ }
+ field = serializers.ChoiceField(
+ choices=[
+ (1, 'Poor quality'),
+ (2, 'Medium quality'),
+ (3, 'Good quality'),
+ ]
+ )
+
+
+class TestChoiceFieldWithListChoices(FieldValues):
+ """
+ Valid and invalid values for a `Choice` field that uses a flat list for the
+ choices, rather than a list of pairs of (`value`, `description`).
+ """
+ valid_inputs = {
+ 'poor': 'poor',
+ 'medium': 'medium',
+ 'good': 'good',
+ }
+ invalid_inputs = {
+ 'awful': ['"awful" is not a valid choice.']
+ }
+ outputs = {
+ 'good': 'good'
+ }
+ field = serializers.ChoiceField(choices=('poor', 'medium', 'good'))
+
+
+class TestMultipleChoiceField(FieldValues):
+ """
+ Valid and invalid values for `MultipleChoiceField`.
+ """
+ valid_inputs = {
+ (): set(),
+ ('aircon',): set(['aircon']),
+ ('aircon', 'manual'): set(['aircon', 'manual']),
+ }
+ invalid_inputs = {
+ 'abc': ['Expected a list of items but got type "str".'],
+ ('aircon', 'incorrect'): ['"incorrect" is not a valid choice.']
+ }
+ outputs = [
+ (['aircon', 'manual'], set(['aircon', 'manual']))
+ ]
+ field = serializers.MultipleChoiceField(
+ choices=[
+ ('aircon', 'AirCon'),
+ ('manual', 'Manual drive'),
+ ('diesel', 'Diesel'),
+ ]
+ )
+
+
+# File serializers...
+
+class MockFile:
+ def __init__(self, name='', size=0, url=''):
+ self.name = name
+ self.size = size
+ self.url = url
+
+ def __eq__(self, other):
+ return (
+ isinstance(other, MockFile) and
+ self.name == other.name and
+ self.size == other.size and
+ self.url == other.url
+ )
+
+
+class TestFileField(FieldValues):
+ """
+ Values for `FileField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example', size=10), MockFile(name='example', size=10))
+ ]
+ invalid_inputs = [
+ ('invalid', ['The submitted data was not a file. Check the encoding type on the form.']),
+ (MockFile(name='example.txt', size=0), ['The submitted file is empty.']),
+ (MockFile(name='', size=10), ['No filename could be determined.']),
+ (MockFile(name='x' * 100, size=10), ['Ensure this filename has at most 10 characters (it has 100).'])
+ ]
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), '/example.txt'),
+ ('', None)
+ ]
+ field = serializers.FileField(max_length=10)
+
+
+class TestFieldFieldWithName(FieldValues):
+ """
+ Values for `FileField` with a filename output instead of URLs.
+ """
+ valid_inputs = {}
+ invalid_inputs = {}
+ outputs = [
+ (MockFile(name='example.txt', url='/example.txt'), 'example.txt')
+ ]
+ field = serializers.FileField(use_url=False)
+
+
+# Stub out mock Django `forms.ImageField` class so we don't *actually*
+# call into it's regular validation, or require PIL for testing.
+class FailImageValidation(object):
+ def to_python(self, value):
+ raise serializers.ValidationError(self.error_messages['invalid_image'])
+
+
+class PassImageValidation(object):
+ def to_python(self, value):
+ return value
+
+
+class TestInvalidImageField(FieldValues):
+ """
+ Values for an invalid `ImageField`.
+ """
+ valid_inputs = {}
+ invalid_inputs = [
+ (MockFile(name='example.txt', size=10), ['Upload a valid image. The file you uploaded was either not an image or a corrupted image.'])
+ ]
+ outputs = {}
+ field = serializers.ImageField(_DjangoImageField=FailImageValidation)
+
+
+class TestValidImageField(FieldValues):
+ """
+ Values for an valid `ImageField`.
+ """
+ valid_inputs = [
+ (MockFile(name='example.txt', size=10), MockFile(name='example.txt', size=10))
+ ]
+ invalid_inputs = {}
+ outputs = {}
+ field = serializers.ImageField(_DjangoImageField=PassImageValidation)
+
+
+# Composite serializers...
+
+class TestListField(FieldValues):
+ """
+ Values for `ListField` with IntegerField as child.
+ """
+ valid_inputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ invalid_inputs = [
+ ('not a list', ['Expected a list of items but got type "str".']),
+ ([1, 2, 'error'], ['A valid integer is required.'])
+ ]
+ outputs = [
+ ([1, 2, 3], [1, 2, 3]),
+ (['1', '2', '3'], [1, 2, 3])
+ ]
+ field = serializers.ListField(child=serializers.IntegerField())
+
+
+class TestUnvalidatedListField(FieldValues):
+ """
+ Values for `ListField` with no `child` argument.
+ """
+ valid_inputs = [
+ ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+ ]
+ invalid_inputs = [
+ ('not a list', ['Expected a list of items but got type "str".']),
+ ]
+ outputs = [
+ ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]),
+ ]
+ field = serializers.ListField()
+
+
+class TestDictField(FieldValues):
+ """
+ Values for `ListField` with CharField as child.
+ """
+ valid_inputs = [
+ ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+ ]
+ invalid_inputs = [
+ ({'a': 1, 'b': None}, ['This field may not be null.']),
+ ('not a dict', ['Expected a dictionary of items but got type "str".']),
+ ]
+ outputs = [
+ ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
+ ]
+ field = serializers.DictField(child=serializers.CharField())
+
+
+class TestUnvalidatedDictField(FieldValues):
+ """
+ Values for `ListField` with no `child` argument.
+ """
+ valid_inputs = [
+ ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),
+ ]
+ invalid_inputs = [
+ ('not a dict', ['Expected a dictionary of items but got type "str".']),
+ ]
+ outputs = [
+ ({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}),
+ ]
+ field = serializers.DictField()
+
+
+# Tests for FieldField.
+# ---------------------
+
+class MockRequest:
+ def build_absolute_uri(self, value):
+ return 'http://example.com' + value
+
+
+class TestFileFieldContext:
+ def test_fully_qualified_when_request_in_context(self):
+ field = serializers.FileField(max_length=10)
+ field._context = {'request': MockRequest()}
+ obj = MockFile(name='example.txt', url='/example.txt')
+ value = field.to_representation(obj)
+ assert value == 'http://example.com/example.txt'
+
+
+# Tests for SerializerMethodField.
+# --------------------------------
+
+class TestSerializerMethodField:
+ def test_serializer_method_field(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField()
+
+ def get_example_field(self, obj):
+ return 'ran get_example_field(%d)' % obj['example_field']
+
+ serializer = ExampleSerializer({'example_field': 123})
+ assert serializer.data == {
+ 'example_field': 'ran get_example_field(123)'
+ }
+
+ def test_redundant_method_name(self):
+ class ExampleSerializer(serializers.Serializer):
+ example_field = serializers.SerializerMethodField('get_example_field')
+
+ with pytest.raises(AssertionError) as exc_info:
+ ExampleSerializer().fields
+ assert str(exc_info.value) == (
+ "It is redundant to specify `get_example_field` on "
+ "SerializerMethodField 'example_field' in serializer "
+ "'ExampleSerializer', because it is the same as the default "
+ "method name. Remove the `method_name` argument."
+ )
diff --git a/tests/test_filters.py b/tests/test_filters.py
new file mode 100644
index 00000000..e7cb0c79
--- /dev/null
+++ b/tests/test_filters.py
@@ -0,0 +1,823 @@
+from __future__ import unicode_literals
+import datetime
+from decimal import Decimal
+from django.db import models
+from django.conf.urls import patterns, url
+from django.core.urlresolvers import reverse
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.utils import unittest
+from django.utils.dateparse import parse_date
+from django.utils.six.moves import reload_module
+from rest_framework import generics, serializers, status, filters
+from rest_framework.compat import django_filters
+from rest_framework.test import APIRequestFactory
+from .models import BaseFilterableItem, FilterableItem, BasicModel
+
+
+factory = APIRequestFactory()
+
+
+if django_filters:
+ class FilterableItemSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FilterableItem
+
+ # Basic filter on a list view.
+ class FilterFieldsRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_fields = ['decimal', 'date']
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These class are used to test a filter class.
+ class SeveralFieldsFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter(lookup_type='icontains')
+ decimal = django_filters.NumberFilter(lookup_type='lt')
+ date = django_filters.DateFilter(lookup_type='gt')
+
+ class Meta:
+ model = FilterableItem
+ fields = ['text', 'decimal', 'date']
+
+ class FilterClassRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These classes are used to test a misconfigured filter class.
+ class MisconfiguredFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter(lookup_type='icontains')
+
+ class Meta:
+ model = BasicModel
+ fields = ['text']
+
+ class IncorrectlyConfiguredRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = MisconfiguredFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ class FilterClassDetailView(generics.RetrieveAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # These classes are used to test base model filter support
+ class BaseFilterableItemFilter(django_filters.FilterSet):
+ text = django_filters.CharFilter()
+
+ class Meta:
+ model = BaseFilterableItem
+
+ class BaseFilterableItemFilterRootView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_class = BaseFilterableItemFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ # Regression test for #814
+ class FilterFieldsQuerysetView(generics.ListCreateAPIView):
+ queryset = FilterableItem.objects.all()
+ serializer_class = FilterableItemSerializer
+ filter_fields = ['decimal', 'date']
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ class GetQuerysetView(generics.ListCreateAPIView):
+ serializer_class = FilterableItemSerializer
+ filter_class = SeveralFieldsFilter
+ filter_backends = (filters.DjangoFilterBackend,)
+
+ def get_queryset(self):
+ return FilterableItem.objects.all()
+
+ urlpatterns = patterns(
+ '',
+ url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
+ url(r'^$', FilterClassRootView.as_view(), name='root-view'),
+ url(r'^get-queryset/$', GetQuerysetView.as_view(),
+ name='get-queryset-view'),
+ )
+
+
+class CommonFilteringTestCase(TestCase):
+ def _serialize_object(self, obj):
+ return {'id': obj.id, 'text': obj.text, 'decimal': str(obj.decimal), 'date': obj.date.isoformat()}
+
+ def setUp(self):
+ """
+ Create 10 FilterableItem instances.
+ """
+ base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8))
+ for i in range(10):
+ text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc.
+ decimal = base_data[1] + i
+ date = base_data[2] - datetime.timedelta(days=i * 2)
+ FilterableItem(text=text, decimal=decimal, date=date).save()
+
+ self.objects = FilterableItem.objects
+ self.data = [
+ self._serialize_object(obj)
+ for obj in self.objects.all()
+ ]
+
+
+class IntegrationTestFiltering(CommonFilteringTestCase):
+ """
+ Integration tests for filtered list views.
+ """
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_fields_root_view(self):
+ """
+ GET requests to paginated ListCreateAPIView should return paginated results.
+ """
+ view = FilterFieldsRootView.as_view()
+
+ # Basic test with no filter.
+ request = factory.get('/')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, self.data)
+
+ # Tests that the decimal filter works.
+ search_decimal = Decimal('2.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the date filter works.
+ search_date = datetime.date(2012, 9, 22)
+ request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22'
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) == search_date]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_filter_with_queryset(self):
+ """
+ Regression test for #814.
+ """
+ view = FilterFieldsQuerysetView.as_view()
+
+ # Tests that the decimal filter works.
+ search_decimal = Decimal('2.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_filter_with_get_queryset_only(self):
+ """
+ Regression test for #834.
+ """
+ view = GetQuerysetView.as_view()
+ request = factory.get('/get-queryset/')
+ view(request).render()
+ # Used to raise "issubclass() arg 2 must be a class or tuple of classes"
+ # here when neither `model' nor `queryset' was specified.
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_class_root_view(self):
+ """
+ GET requests to filtered ListCreateAPIView that have a filter_class set
+ should return filtered results.
+ """
+ view = FilterClassRootView.as_view()
+
+ # Basic test with no filter.
+ request = factory.get('/')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, self.data)
+
+ # Tests that the decimal filter set with 'lt' in the filter class works.
+ search_decimal = Decimal('4.25')
+ request = factory.get('/', {'decimal': '%s' % search_decimal})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the date filter set with 'gt' in the filter class works.
+ search_date = datetime.date(2012, 10, 2)
+ request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02'
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that the text filter set with 'icontains' in the filter class works.
+ search_text = 'ff'
+ request = factory.get('/', {'text': '%s' % search_text})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if search_text in f['text'].lower()]
+ self.assertEqual(response.data, expected_data)
+
+ # Tests that multiple filters works.
+ search_decimal = Decimal('5.25')
+ search_date = datetime.date(2012, 10, 2)
+ request = factory.get('/', {
+ 'decimal': '%s' % (search_decimal,),
+ 'date': '%s' % (search_date,)
+ })
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ expected_data = [f for f in self.data if parse_date(f['date']) > search_date and
+ Decimal(f['decimal']) < search_decimal]
+ self.assertEqual(response.data, expected_data)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_incorrectly_configured_filter(self):
+ """
+ An error should be displayed when the filter class is misconfigured.
+ """
+ view = IncorrectlyConfiguredRootView.as_view()
+
+ request = factory.get('/')
+ self.assertRaises(AssertionError, view, request)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_base_model_filter(self):
+ """
+ The `get_filter_class` model checks should allow base model filters.
+ """
+ view = BaseFilterableItemFilterRootView.as_view()
+
+ request = factory.get('/?text=aaa')
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(len(response.data), 1)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_unknown_filter(self):
+ """
+ GET requests with filters that aren't configured should return 200.
+ """
+ view = FilterFieldsRootView.as_view()
+
+ search_integer = 10
+ request = factory.get('/', {'integer': '%s' % search_integer})
+ response = view(request).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+
+class IntegrationTestDetailFiltering(CommonFilteringTestCase):
+ """
+ Integration tests for filtered detail views.
+ """
+ urls = 'tests.test_filters'
+
+ def _get_url(self, item):
+ return reverse('detail-view', kwargs=dict(pk=item.pk))
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_get_filtered_detail_view(self):
+ """
+ GET requests to filtered RetrieveAPIView that have a filter_class set
+ should return filtered results.
+ """
+ item = self.objects.all()[0]
+ data = self._serialize_object(item)
+
+ # Basic test with no filter.
+ response = self.client.get(self._get_url(item))
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, data)
+
+ # Tests that the decimal filter set that should fail.
+ search_decimal = Decimal('4.25')
+ high_item = self.objects.filter(decimal__gt=search_decimal)[0]
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(high_item)),
+ {'decimal': '{param}'.format(param=search_decimal)})
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Tests that the decimal filter set that should succeed.
+ search_decimal = Decimal('4.25')
+ low_item = self.objects.filter(decimal__lt=search_decimal)[0]
+ low_item_data = self._serialize_object(low_item)
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(low_item)),
+ {'decimal': '{param}'.format(param=search_decimal)})
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, low_item_data)
+
+ # Tests that multiple filters works.
+ search_decimal = Decimal('5.25')
+ search_date = datetime.date(2012, 10, 2)
+ valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0]
+ valid_item_data = self._serialize_object(valid_item)
+ response = self.client.get(
+ '{url}'.format(url=self._get_url(valid_item)), {
+ 'decimal': '{decimal}'.format(decimal=search_decimal),
+ 'date': '{date}'.format(date=search_date)
+ })
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, valid_item_data)
+
+
+class SearchFilterModel(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+
+
+class SearchFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SearchFilterModel
+
+
+class SearchFilterTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text is:
+ #
+ # z abc
+ # zz bcd
+ # zzz cde
+ # ...
+ for idx in range(10):
+ title = 'z' * (idx + 1)
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ SearchFilterModel(title=title, text=text).save()
+
+ def test_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'z', 'text': 'abc'},
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ def test_exact_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('=title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'zzz'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'zzz', 'text': 'cde'}
+ ]
+ )
+
+ def test_startswith_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', '^text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ def test_search_with_nonstandard_search_param(self):
+ with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}):
+ reload_module(filters)
+
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModel.objects.all()
+ serializer_class = SearchFilterSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('title', 'text')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'query': 'b'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'z', 'text': 'abc'},
+ {'id': 2, 'title': 'zz', 'text': 'bcd'}
+ ]
+ )
+
+ reload_module(filters)
+
+
+class AttributeModel(models.Model):
+ label = models.CharField(max_length=32)
+
+
+class SearchFilterModelM2M(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+ attributes = models.ManyToManyField(AttributeModel)
+
+
+class SearchFilterM2MSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = SearchFilterModelM2M
+
+
+class SearchFilterM2MTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text/attributes is:
+ #
+ # z abc [1, 2, 3]
+ # zz bcd [1, 2, 3]
+ # zzz cde [1, 2, 3]
+ # ...
+ for idx in range(3):
+ label = 'w' * (idx + 1)
+ AttributeModel(label=label)
+
+ for idx in range(10):
+ title = 'z' * (idx + 1)
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ SearchFilterModelM2M(title=title, text=text).save()
+ SearchFilterModelM2M.objects.get(title='zz').attributes.add(1, 2, 3)
+
+ def test_m2m_search(self):
+ class SearchListView(generics.ListAPIView):
+ queryset = SearchFilterModelM2M.objects.all()
+ serializer_class = SearchFilterM2MSerializer
+ filter_backends = (filters.SearchFilter,)
+ search_fields = ('=title', 'text', 'attributes__label')
+
+ view = SearchListView.as_view()
+ request = factory.get('/', {'search': 'zz'})
+ response = view(request)
+ self.assertEqual(len(response.data), 1)
+
+
+class OrderingFilterModel(models.Model):
+ title = models.CharField(max_length=20)
+ text = models.CharField(max_length=100)
+
+
+class OrderingFilterRelatedModel(models.Model):
+ related_object = models.ForeignKey(OrderingFilterModel,
+ related_name="relateds")
+
+
+class OrderingFilterSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OrderingFilterModel
+
+
+class DjangoFilterOrderingModel(models.Model):
+ date = models.DateField()
+ text = models.CharField(max_length=10)
+
+ class Meta:
+ ordering = ['-date']
+
+
+class DjangoFilterOrderingSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DjangoFilterOrderingModel
+
+
+class DjangoFilterOrderingTests(TestCase):
+ def setUp(self):
+ data = [{
+ 'date': datetime.date(2012, 10, 8),
+ 'text': 'abc'
+ }, {
+ 'date': datetime.date(2013, 10, 8),
+ 'text': 'bcd'
+ }, {
+ 'date': datetime.date(2014, 10, 8),
+ 'text': 'cde'
+ }]
+
+ for d in data:
+ DjangoFilterOrderingModel.objects.create(**d)
+
+ @unittest.skipUnless(django_filters, 'django-filter not installed')
+ def test_default_ordering(self):
+ class DjangoFilterOrderingView(generics.ListAPIView):
+ serializer_class = DjangoFilterOrderingSerializer
+ queryset = DjangoFilterOrderingModel.objects.all()
+ filter_backends = (filters.DjangoFilterBackend,)
+ filter_fields = ['text']
+ ordering = ('-date',)
+
+ view = DjangoFilterOrderingView.as_view()
+ request = factory.get('/')
+ response = view(request)
+
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'date': '2014-10-08', 'text': 'cde'},
+ {'id': 2, 'date': '2013-10-08', 'text': 'bcd'},
+ {'id': 1, 'date': '2012-10-08', 'text': 'abc'}
+ ]
+ )
+
+
+class OrderingFilterTests(TestCase):
+ def setUp(self):
+ # Sequence of title/text is:
+ #
+ # zyx abc
+ # yxw bcd
+ # xwv cde
+ for idx in range(3):
+ title = (
+ chr(ord('z') - idx) +
+ chr(ord('y') - idx) +
+ chr(ord('x') - idx)
+ )
+ text = (
+ chr(idx + ord('a')) +
+ chr(idx + ord('b')) +
+ chr(idx + ord('c'))
+ )
+ OrderingFilterModel(title=title, text=text).save()
+
+ def test_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ ]
+ )
+
+ def test_reverse_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': '-text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_incorrectfield_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'foobar'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ oredering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_default_ordering_using_string(self):
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('')
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ ]
+ )
+
+ def test_ordering_by_aggregate_field(self):
+ # create some related models to aggregate order by
+ num_objs = [2, 5, 3]
+ for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
+ num_objs):
+ for _ in range(num_relateds):
+ new_related = OrderingFilterRelatedModel(
+ related_object=obj
+ )
+ new_related.save()
+
+ class OrderingListView(generics.ListAPIView):
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = 'title'
+ ordering_fields = '__all__'
+ queryset = OrderingFilterModel.objects.all().annotate(
+ models.Count("relateds"))
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'relateds__count'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ ]
+ )
+
+ def test_ordering_with_nonstandard_ordering_param(self):
+ with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}):
+ reload_module(filters)
+
+ class OrderingListView(generics.ListAPIView):
+ queryset = OrderingFilterModel.objects.all()
+ serializer_class = OrderingFilterSerializer
+ filter_backends = (filters.OrderingFilter,)
+ ordering = ('title',)
+ ordering_fields = ('text',)
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'order': 'text'})
+ response = view(request)
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, 'title': 'zyx', 'text': 'abc'},
+ {'id': 2, 'title': 'yxw', 'text': 'bcd'},
+ {'id': 3, 'title': 'xwv', 'text': 'cde'},
+ ]
+ )
+
+ reload_module(filters)
+
+
+class SensitiveOrderingFilterModel(models.Model):
+ username = models.CharField(max_length=20)
+ password = models.CharField(max_length=100)
+
+
+# Three different styles of serializer.
+# All should allow ordering by username, but not by password.
+class SensitiveDataSerializer1(serializers.ModelSerializer):
+ username = serializers.CharField()
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'username')
+
+
+class SensitiveDataSerializer2(serializers.ModelSerializer):
+ username = serializers.CharField()
+ password = serializers.CharField(write_only=True)
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'username', 'password')
+
+
+class SensitiveDataSerializer3(serializers.ModelSerializer):
+ user = serializers.CharField(source='username')
+
+ class Meta:
+ model = SensitiveOrderingFilterModel
+ fields = ('id', 'user')
+
+
+class SensitiveOrderingFilterTests(TestCase):
+ def setUp(self):
+ for idx in range(3):
+ username = {0: 'userA', 1: 'userB', 2: 'userC'}[idx]
+ password = {0: 'passA', 1: 'passC', 2: 'passB'}[idx]
+ SensitiveOrderingFilterModel(username=username, password=password).save()
+
+ def test_order_by_serializer_fields(self):
+ for serializer_cls in [
+ SensitiveDataSerializer1,
+ SensitiveDataSerializer2,
+ SensitiveDataSerializer3
+ ]:
+ class OrderingListView(generics.ListAPIView):
+ queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
+ filter_backends = (filters.OrderingFilter,)
+ serializer_class = serializer_cls
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': '-username'})
+ response = view(request)
+
+ if serializer_cls == SensitiveDataSerializer3:
+ username_field = 'user'
+ else:
+ username_field = 'username'
+
+ # Note: Inverse username ordering correctly applied.
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, username_field: 'userC'},
+ {'id': 2, username_field: 'userB'},
+ {'id': 1, username_field: 'userA'},
+ ]
+ )
+
+ def test_cannot_order_by_non_serializer_fields(self):
+ for serializer_cls in [
+ SensitiveDataSerializer1,
+ SensitiveDataSerializer2,
+ SensitiveDataSerializer3
+ ]:
+ class OrderingListView(generics.ListAPIView):
+ queryset = SensitiveOrderingFilterModel.objects.all().order_by('username')
+ filter_backends = (filters.OrderingFilter,)
+ serializer_class = serializer_cls
+
+ view = OrderingListView.as_view()
+ request = factory.get('/', {'ordering': 'password'})
+ response = view(request)
+
+ if serializer_cls == SensitiveDataSerializer3:
+ username_field = 'user'
+ else:
+ username_field = 'username'
+
+ # Note: The passwords are not in order. Default ordering is used.
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 1, username_field: 'userA'}, # PassB
+ {'id': 2, username_field: 'userB'}, # PassC
+ {'id': 3, username_field: 'userC'}, # PassA
+ ]
+ )
diff --git a/rest_framework/tests/test_generics.py b/tests/test_generics.py
index 1550880b..88e792ce 100644
--- a/rest_framework/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -1,46 +1,75 @@
from __future__ import unicode_literals
+import django
from django.db import models
from django.shortcuts import get_object_or_404
from django.test import TestCase
+from django.utils import six
from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
-from rest_framework.compat import six
+from tests.models import BasicModel, RESTFrameworkModel
+from tests.models import ForeignKeySource, ForeignKeyTarget
factory = APIRequestFactory()
-class RootView(generics.ListCreateAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
+# Models
+class SlugBasedModel(RESTFrameworkModel):
+ text = models.CharField(max_length=100)
+ slug = models.SlugField(max_length=32)
-class InstanceView(generics.RetrieveUpdateDestroyAPIView):
- """
- Example description for OPTIONS.
- """
- model = BasicModel
+# Model for regression test for #285
+class Comment(RESTFrameworkModel):
+ email = models.EmailField()
+ content = models.CharField(max_length=200)
+ created = models.DateTimeField(auto_now_add=True)
+
+
+# Serializers
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class ForeignKeySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
class SlugSerializer(serializers.ModelSerializer):
- slug = serializers.Field() # read only
+ slug = serializers.ReadOnlyField()
class Meta:
model = SlugBasedModel
- exclude = ('id',)
+ fields = ('text', 'slug')
+
+
+# Views
+class RootView(generics.ListCreateAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+
+
+class InstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicModel.objects.exclude(text='filtered out')
+ serializer_class = BasicSerializer
+
+
+class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ForeignKeySource.objects.all()
+ serializer_class = ForeignKeySerializer
class SlugBasedInstanceView(InstanceView):
"""
A model with a slug-field.
"""
- model = SlugBasedModel
+ queryset = SlugBasedModel.objects.all()
serializer_class = SlugSerializer
lookup_field = 'slug'
+# Tests
class TestRootView(TestCase):
def setUp(self):
"""
@@ -88,7 +117,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'PUT' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'})
def test_delete_root_view(self):
"""
@@ -98,48 +127,7 @@ class TestRootView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'DELETE' not allowed."})
-
- def test_options_root_view(self):
- """
- OPTIONS requests to ListCreateAPIView should return metadata
- """
- request = factory.options('/')
- with self.assertNumQueries(0):
- response = self.view(request).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Root',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'POST': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- "label": "Text comes here",
- "help_text": "Text description."
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
+ self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'})
def test_post_cannot_set_id(self):
"""
@@ -155,15 +143,18 @@ class TestRootView(TestCase):
self.assertEqual(created.text, 'foobar')
+EXPECTED_QUERIES_FOR_PUT = 3 if django.VERSION < (1, 6) else 2
+
+
class TestInstanceView(TestCase):
def setUp(self):
"""
- Create 3 BasicModel intances.
+ Create 3 BasicModel instances.
"""
- items = ['foo', 'bar', 'baz']
+ items = ['foo', 'bar', 'baz', 'filtered out']
for item in items:
BasicModel(text=item).save()
- self.objects = BasicModel.objects
+ self.objects = BasicModel.objects.exclude(text='filtered out')
self.data = [
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
@@ -190,7 +181,7 @@ class TestInstanceView(TestCase):
with self.assertNumQueries(0):
response = self.view(request).render()
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
- self.assertEqual(response.data, {"detail": "Method 'POST' not allowed."})
+ self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'})
def test_put_instance_view(self):
"""
@@ -198,10 +189,10 @@ class TestInstanceView(TestCase):
"""
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk='1').render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
+ self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'})
updated = self.objects.get(id=1)
self.assertEqual(updated.text, 'foobar')
@@ -212,7 +203,7 @@ class TestInstanceView(TestCase):
data = {'text': 'foobar'}
request = factory.patch('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -231,47 +222,6 @@ class TestInstanceView(TestCase):
ids = [obj.id for obj in self.objects.all()]
self.assertEqual(ids, [2, 3])
- def test_options_instance_view(self):
- """
- OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
- """
- request = factory.options('/1')
- with self.assertNumQueries(1):
- response = self.view(request, pk=1).render()
- expected = {
- 'parses': [
- 'application/json',
- 'application/x-www-form-urlencoded',
- 'multipart/form-data'
- ],
- 'renders': [
- 'application/json',
- 'text/html'
- ],
- 'name': 'Instance',
- 'description': 'Example description for OPTIONS.',
- 'actions': {
- 'PUT': {
- 'text': {
- 'max_length': 100,
- 'read_only': False,
- 'required': True,
- 'type': 'string',
- 'label': 'Text comes here',
- 'help_text': 'Text description.'
- },
- 'id': {
- 'read_only': True,
- 'required': False,
- 'type': 'integer',
- 'label': 'ID',
- },
- }
- }
- }
- self.assertEqual(response.status_code, status.HTTP_200_OK)
- self.assertEqual(response.data, expected)
-
def test_get_instance_view_incorrect_arg(self):
"""
GET requests with an incorrect pk type, should raise 404, not 500.
@@ -288,7 +238,7 @@ class TestInstanceView(TestCase):
"""
data = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(2):
+ with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT):
response = self.view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
@@ -297,46 +247,56 @@ class TestInstanceView(TestCase):
def test_put_to_deleted_instance(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- if it does not currently exist.
+ PUT requests to RetrieveUpdateDestroyAPIView should return 404 if
+ an object does not currently exist.
"""
self.objects.get(id=1).delete()
data = {'text': 'foobar'}
request = factory.put('/1', data, format='json')
- with self.assertNumQueries(3):
+ with self.assertNumQueries(1):
response = self.view(request, pk=1).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'id': 1, 'text': 'foobar'})
- updated = self.objects.get(id=1)
- self.assertEqual(updated.text, 'foobar')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- def test_put_as_create_on_id_based_url(self):
+ def test_put_to_filtered_out_instance(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if it doesn't exist.
+ PUT requests to an URL of instance which is filtered out should not be
+ able to create new objects.
"""
- data = {'text': 'foobar'}
- # pk fields can not be created on demand, only the database can set the pk for a new object
- request = factory.put('/5', data, format='json')
- with self.assertNumQueries(3):
- response = self.view(request, pk=5).render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- new_obj = self.objects.get(pk=5)
- self.assertEqual(new_obj.text, 'foobar')
+ data = {'text': 'foo'}
+ filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk
+ request = factory.put('/{0}'.format(filtered_out_pk), data, format='json')
+ response = self.view(request, pk=filtered_out_pk).render()
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- def test_put_as_create_on_slug_based_url(self):
+ def test_patch_cannot_create_an_object(self):
"""
- PUT requests to RetrieveUpdateDestroyAPIView should create an object
- at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
+ PATCH requests should not be able to create objects.
"""
data = {'text': 'foobar'}
- request = factory.put('/test_slug', data, format='json')
- with self.assertNumQueries(2):
- response = self.slug_based_view(request, slug='test_slug').render()
- self.assertEqual(response.status_code, status.HTTP_201_CREATED)
- self.assertEqual(response.data, {'slug': 'test_slug', 'text': 'foobar'})
- new_obj = SlugBasedModel.objects.get(slug='test_slug')
- self.assertEqual(new_obj.text, 'foobar')
+ request = factory.patch('/999', data, format='json')
+ with self.assertNumQueries(1):
+ response = self.view(request, pk=999).render()
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+ self.assertFalse(self.objects.filter(id=999).exists())
+
+
+class TestFKInstanceView(TestCase):
+ def setUp(self):
+ """
+ Create 3 BasicModel instances.
+ """
+ items = ['foo', 'bar', 'baz']
+ for item in items:
+ t = ForeignKeyTarget(name=item)
+ t.save()
+ ForeignKeySource(name='source_' + item, target=t).save()
+
+ self.objects = ForeignKeySource.objects
+ self.data = [
+ {'id': obj.id, 'name': obj.name}
+ for obj in self.objects.all()
+ ]
+ self.view = FKInstanceView.as_view()
class TestOverriddenGetObject(TestCase):
@@ -344,9 +304,10 @@ class TestOverriddenGetObject(TestCase):
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
queryset/model mechanism but instead overrides get_object()
"""
+
def setUp(self):
"""
- Create 3 BasicModel intances.
+ Create 3 BasicModel instances.
"""
items = ['foo', 'bar', 'baz']
for item in items:
@@ -361,7 +322,7 @@ class TestOverriddenGetObject(TestCase):
"""
Example detail view for override of get_object().
"""
- model = BasicModel
+ serializer_class = BasicSerializer
def get_object(self):
pk = int(self.kwargs['pk'])
@@ -419,11 +380,13 @@ class ClassB(models.Model):
class ClassA(models.Model):
name = models.CharField(max_length=255)
- childs = models.ManyToManyField(ClassB, blank=True, null=True)
+ children = models.ManyToManyField(ClassB, blank=True, null=True)
class ClassASerializer(serializers.ModelSerializer):
- childs = serializers.PrimaryKeyRelatedField(many=True, source='childs')
+ children = serializers.PrimaryKeyRelatedField(
+ many=True, queryset=ClassB.objects.all()
+ )
class Meta:
model = ClassA
@@ -431,11 +394,11 @@ class ClassASerializer(serializers.ModelSerializer):
class ExampleView(generics.ListCreateAPIView):
serializer_class = ClassASerializer
- model = ClassA
+ queryset = ClassA.objects.all()
-class TestM2MBrowseableAPI(TestCase):
- def test_m2m_in_browseable_api(self):
+class TestM2MBrowsableAPI(TestCase):
+ def test_m2m_in_browsable_api(self):
"""
Test for particularly ugly regression with m2m in browsable API
"""
@@ -455,8 +418,29 @@ class ExclusiveFilterBackend(object):
return queryset.filter(text='other')
-class TestFilterBackendAppliedToViews(TestCase):
+class TwoFieldModel(models.Model):
+ field_a = models.CharField(max_length=100)
+ field_b = models.CharField(max_length=100)
+
+class DynamicSerializerView(generics.ListCreateAPIView):
+ queryset = TwoFieldModel.objects.all()
+ renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
+
+ def get_serializer_class(self):
+ if self.request.method == 'POST':
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ fields = ('field_b',)
+ else:
+ class DynamicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TwoFieldModel
+ return DynamicSerializer
+
+
+class TestFilterBackendAppliedToViews(TestCase):
def setUp(self):
"""
Create 3 BasicModel instances to filter on.
@@ -499,7 +483,7 @@ class TestFilterBackendAppliedToViews(TestCase):
request = factory.get('/1')
response = instance_view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- self.assertEqual(response.data, {'detail': 'Not found'})
+ self.assertEqual(response.data, {'detail': 'Not found.'})
def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self):
"""
@@ -511,28 +495,6 @@ class TestFilterBackendAppliedToViews(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
-
-class TwoFieldModel(models.Model):
- field_a = models.CharField(max_length=100)
- field_b = models.CharField(max_length=100)
-
-
-class DynamicSerializerView(generics.ListCreateAPIView):
- model = TwoFieldModel
- renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
-
- def get_serializer_class(self):
- if self.request.method == 'POST':
- class DynamicSerializer(serializers.ModelSerializer):
- class Meta:
- model = TwoFieldModel
- fields = ('field_b',)
- return DynamicSerializer
- return super(DynamicSerializerView, self).get_serializer_class()
-
-
-class TestFilterBackendAppliedToViews(TestCase):
-
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
diff --git a/rest_framework/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py
index 8957a43c..a33b832f 100644
--- a/rest_framework/tests/test_htmlrenderer.py
+++ b/tests/test_htmlrenderer.py
@@ -1,15 +1,15 @@
from __future__ import unicode_literals
from django.core.exceptions import PermissionDenied
+from django.conf.urls import patterns, url
from django.http import Http404
from django.test import TestCase
from django.template import TemplateDoesNotExist, Template
-import django.template.loader
+from django.utils import six
from rest_framework import status
-from rest_framework.compat import patterns, url
from rest_framework.decorators import api_view, renderer_classes
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.response import Response
-from rest_framework.compat import six
+import django.template.loader
@api_view(('GET',))
@@ -34,7 +34,8 @@ def not_found(request):
raise Http404()
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^$', example),
url(r'^permission_denied$', permission_denied),
url(r'^not_found$', not_found),
@@ -42,7 +43,7 @@ urlpatterns = patterns('',
class TemplateHTMLRendererTests(TestCase):
- urls = 'rest_framework.tests.test_htmlrenderer'
+ urls = 'tests.test_htmlrenderer'
def setUp(self):
"""
@@ -50,12 +51,18 @@ class TemplateHTMLRendererTests(TestCase):
"""
self.get_template = django.template.loader.get_template
- def get_template(template_name):
+ def get_template(template_name, dirs=None):
if template_name == 'example.html':
return Template("example: {{ object }}")
raise TemplateDoesNotExist(template_name)
+ def select_template(template_name_list, dirs=None, using=None):
+ if template_name_list == ['example.html']:
+ return Template("example: {{ object }}")
+ raise TemplateDoesNotExist(template_name_list[0])
+
django.template.loader.get_template = get_template
+ django.template.loader.select_template = select_template
def tearDown(self):
"""
@@ -82,7 +89,7 @@ class TemplateHTMLRendererTests(TestCase):
class TemplateHTMLRendererExceptionTests(TestCase):
- urls = 'rest_framework.tests.test_htmlrenderer'
+ urls = 'tests.test_htmlrenderer'
def setUp(self):
"""
@@ -108,11 +115,13 @@ class TemplateHTMLRendererExceptionTests(TestCase):
def test_not_found_html_view_with_template(self):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
- self.assertEqual(response.content, six.b("404: Not found"))
+ self.assertTrue(response.content in (
+ six.b("404: Not found"), six.b("404 Not Found")))
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
- self.assertEqual(response.content, six.b("403: Permission denied"))
+ self.assertTrue(response.content in (
+ six.b("403: Permission denied"), six.b("403 Forbidden")))
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
new file mode 100644
index 00000000..3a435f02
--- /dev/null
+++ b/tests/test_metadata.py
@@ -0,0 +1,209 @@
+from __future__ import unicode_literals
+from rest_framework import exceptions, serializers, status, views, versioning
+from rest_framework.request import Request
+from rest_framework.renderers import BrowsableAPIRenderer
+from rest_framework.test import APIRequestFactory
+
+request = Request(APIRequestFactory().options('/'))
+
+
+class TestMetadata:
+ def test_metadata(self):
+ """
+ OPTIONS requests to views should return a valid 200 response.
+ """
+ class ExampleView(views.APIView):
+ """Example view."""
+ pass
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ]
+ }
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == expected
+
+ def test_none_metadata(self):
+ """
+ OPTIONS requests to views where `metadata_class = None` should raise
+ a MethodNotAllowed exception, which will result in an HTTP 405 response.
+ """
+ class ExampleView(views.APIView):
+ metadata_class = None
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
+ assert response.data == {'detail': 'Method "OPTIONS" not allowed.'}
+
+ def test_actions(self):
+ """
+ On generic views OPTIONS should return an 'actions' key with metadata
+ on the fields that may be supplied to PUT and POST requests.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(
+ min_value=1, max_value=1000
+ )
+ char_field = serializers.CharField(
+ required=False, min_length=3, max_length=40
+ )
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ expected = {
+ 'name': 'Example',
+ 'description': 'Example view.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ],
+ 'actions': {
+ 'POST': {
+ 'choice_field': {
+ 'type': 'choice',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Choice field',
+ 'choices': [
+ {'display_name': 'red', 'value': 'red'},
+ {'display_name': 'green', 'value': 'green'},
+ {'display_name': 'blue', 'value': 'blue'}
+ ]
+ },
+ 'integer_field': {
+ 'type': 'integer',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Integer field',
+ 'min_value': 1,
+ 'max_value': 1000,
+
+ },
+ 'char_field': {
+ 'type': 'string',
+ 'required': False,
+ 'read_only': False,
+ 'label': 'Char field',
+ 'min_length': 3,
+ 'max_length': 40
+ }
+ }
+ }
+ }
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == expected
+
+ def test_global_permissions(self):
+ """
+ If a user does not have global permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def check_permissions(self, request):
+ if request.method == 'POST':
+ raise exceptions.PermissionDenied()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_200_OK
+ assert list(response.data['actions'].keys()) == ['PUT']
+
+ def test_object_permissions(self):
+ """
+ If a user does not have object permissions on an action, then any
+ metadata associated with it should not be included in OPTION responses.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ choice_field = serializers.ChoiceField(['red', 'green', 'blue'])
+ integer_field = serializers.IntegerField(max_value=10)
+ char_field = serializers.CharField(required=False)
+
+ class ExampleView(views.APIView):
+ """Example view."""
+ def post(self, request):
+ pass
+
+ def put(self, request):
+ pass
+
+ def get_serializer(self):
+ return ExampleSerializer()
+
+ def get_object(self):
+ if self.request.method == 'PUT':
+ raise exceptions.PermissionDenied()
+
+ view = ExampleView.as_view()
+ response = view(request=request)
+ assert response.status_code == status.HTTP_200_OK
+ assert list(response.data['actions'].keys()) == ['POST']
+
+ def test_bug_2455_clone_request(self):
+ class ExampleView(views.APIView):
+ renderer_classes = (BrowsableAPIRenderer,)
+
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ assert hasattr(self.request, 'version')
+ return serializers.Serializer()
+
+ view = ExampleView.as_view()
+ view(request=request)
+
+ def test_bug_2477_clone_request(self):
+ class ExampleView(views.APIView):
+ renderer_classes = (BrowsableAPIRenderer,)
+
+ def post(self, request):
+ pass
+
+ def get_serializer(self):
+ assert hasattr(self.request, 'versioning_scheme')
+ return serializers.Serializer()
+
+ scheme = versioning.QueryParameterVersioning
+ view = ExampleView.as_view(versioning_class=scheme)
+ view(request=request)
diff --git a/tests/test_middleware.py b/tests/test_middleware.py
new file mode 100644
index 00000000..4c099fca
--- /dev/null
+++ b/tests/test_middleware.py
@@ -0,0 +1,37 @@
+
+from django.conf.urls import patterns, url
+from django.contrib.auth.models import User
+from rest_framework.authentication import TokenAuthentication
+from rest_framework.authtoken.models import Token
+from rest_framework.test import APITestCase
+from rest_framework.views import APIView
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', APIView.as_view(authentication_classes=(TokenAuthentication,))),
+)
+
+
+class MyMiddleware(object):
+
+ def process_response(self, request, response):
+ assert hasattr(request, 'user'), '`user` is not set on request'
+ assert request.user.is_authenticated(), '`user` is not authenticated'
+ return response
+
+
+class TestMiddleware(APITestCase):
+
+ urls = 'tests.test_middleware'
+
+ def test_middleware_can_access_user_when_processing_response(self):
+ user = User.objects.create_user('john', 'john@example.com', 'password')
+ key = 'abcd1234'
+ Token.objects.create(key=key, user=user)
+
+ with self.settings(
+ MIDDLEWARE_CLASSES=('tests.test_middleware.MyMiddleware',)
+ ):
+ auth = 'Token ' + key
+ self.client.get('/', HTTP_AUTHORIZATION=auth)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
new file mode 100644
index 00000000..bce2008a
--- /dev/null
+++ b/tests/test_model_serializer.py
@@ -0,0 +1,641 @@
+"""
+The `ModelSerializer` and `HyperlinkedModelSerializer` classes are essentially
+shortcuts for automatically creating serializers based on a given model class.
+
+These tests deal with ensuring that we correctly map the model fields onto
+an appropriate set of serializer fields for each case.
+"""
+from __future__ import unicode_literals
+from django.core.exceptions import ImproperlyConfigured
+from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator
+from django.db import models
+from django.test import TestCase
+from django.utils import six
+from rest_framework import serializers
+from rest_framework.compat import unicode_repr
+
+
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Tests for regular field mappings.
+# ---------------------------------
+
+class CustomField(models.Field):
+ """
+ A custom model field simply for testing purposes.
+ """
+ pass
+
+
+class OneFieldModel(models.Model):
+ char_field = models.CharField(max_length=100)
+
+
+class RegularFieldsModel(models.Model):
+ """
+ A model class for testing regular flat fields.
+ """
+ auto_field = models.AutoField(primary_key=True)
+ big_integer_field = models.BigIntegerField()
+ boolean_field = models.BooleanField(default=False)
+ char_field = models.CharField(max_length=100)
+ comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=100)
+ date_field = models.DateField()
+ datetime_field = models.DateTimeField()
+ decimal_field = models.DecimalField(max_digits=3, decimal_places=1)
+ email_field = models.EmailField(max_length=100)
+ float_field = models.FloatField()
+ integer_field = models.IntegerField()
+ null_boolean_field = models.NullBooleanField()
+ positive_integer_field = models.PositiveIntegerField()
+ positive_small_integer_field = models.PositiveSmallIntegerField()
+ slug_field = models.SlugField(max_length=100)
+ small_integer_field = models.SmallIntegerField()
+ text_field = models.TextField()
+ time_field = models.TimeField()
+ url_field = models.URLField(max_length=100)
+ custom_field = CustomField()
+
+ def method(self):
+ return 'method'
+
+
+COLOR_CHOICES = (('red', 'Red'), ('blue', 'Blue'), ('green', 'Green'))
+
+
+class FieldOptionsModel(models.Model):
+ value_limit_field = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)])
+ length_limit_field = models.CharField(validators=[MinLengthValidator(3)], max_length=12)
+ blank_field = models.CharField(blank=True, max_length=10)
+ null_field = models.IntegerField(null=True)
+ default_field = models.IntegerField(default=0)
+ descriptive_field = models.IntegerField(help_text='Some help text', verbose_name='A label')
+ choices_field = models.CharField(max_length=100, choices=COLOR_CHOICES)
+
+
+class TestModelSerializer(TestCase):
+ def test_create_method(self):
+ class TestSerializer(serializers.ModelSerializer):
+ non_model_field = serializers.CharField()
+
+ class Meta:
+ model = OneFieldModel
+ fields = ('char_field', 'non_model_field')
+
+ serializer = TestSerializer(data={
+ 'char_field': 'foo',
+ 'non_model_field': 'bar',
+ })
+ serializer.is_valid()
+ with self.assertRaises(TypeError) as excinfo:
+ serializer.save()
+ msginitial = 'Got a `TypeError` when calling `OneFieldModel.objects.create()`.'
+ assert str(excinfo.exception).startswith(msginitial)
+
+
+class TestRegularFieldMappings(TestCase):
+ def test_regular_fields(self):
+ """
+ Model fields should map to their equivelent serializer fields.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ big_integer_field = IntegerField()
+ boolean_field = BooleanField(required=False)
+ char_field = CharField(max_length=100)
+ comma_separated_integer_field = CharField(max_length=100, validators=[<django.core.validators.RegexValidator object>])
+ date_field = DateField()
+ datetime_field = DateTimeField()
+ decimal_field = DecimalField(decimal_places=1, max_digits=3)
+ email_field = EmailField(max_length=100)
+ float_field = FloatField()
+ integer_field = IntegerField()
+ null_boolean_field = NullBooleanField(required=False)
+ positive_integer_field = IntegerField()
+ positive_small_integer_field = IntegerField()
+ slug_field = SlugField(max_length=100)
+ small_integer_field = IntegerField()
+ text_field = CharField(style={'base_template': 'textarea.html'})
+ time_field = TimeField()
+ url_field = URLField(max_length=100)
+ custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_field_options(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = FieldOptionsModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ value_limit_field = IntegerField(max_value=10, min_value=1)
+ length_limit_field = CharField(max_length=12, min_length=3)
+ blank_field = CharField(allow_blank=True, max_length=10, required=False)
+ null_field = IntegerField(allow_null=True, required=False)
+ default_field = IntegerField(required=False)
+ descriptive_field = IntegerField(help_text='Some help text', label='A label')
+ choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')])
+ """)
+ if six.PY2:
+ # This particular case is too awkward to resolve fully across
+ # both py2 and py3.
+ expected = expected.replace(
+ "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')",
+ "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')"
+ )
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_method_field(self):
+ """
+ Properties and methods on the model should be allowed as `Meta.fields`
+ values, and should map to `ReadOnlyField`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'method')
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ method = ReadOnlyField()
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_pk_fields(self):
+ """
+ Both `pk` and the actual primary key name are valid in `Meta.fields`.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('pk', 'auto_field')
+
+ expected = dedent("""
+ TestSerializer():
+ pk = IntegerField(label='Auto field', read_only=True)
+ auto_field = IntegerField(read_only=True)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_extra_field_kwargs(self):
+ """
+ Ensure `extra_kwargs` are passed to generated fields.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'char_field')
+ extra_kwargs = {'char_field': {'default': 'extra'}}
+
+ expected = dedent("""
+ TestSerializer():
+ auto_field = IntegerField(read_only=True)
+ char_field = CharField(default='extra', max_length=100)
+ """)
+ self.assertEqual(repr(TestSerializer()), expected)
+
+ def test_invalid_field(self):
+ """
+ Field names that do not map to a model field or relationship should
+ raise a configuration errror.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field', 'invalid')
+
+ with self.assertRaises(ImproperlyConfigured) as excinfo:
+ TestSerializer().fields
+ expected = 'Field name `invalid` is not valid for model `RegularFieldsModel`.'
+ assert str(excinfo.exception) == expected
+
+ def test_missing_field(self):
+ """
+ Fields that have been declared on the serializer class must be included
+ in the `Meta.fields` if it exists.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field',)
+
+ with self.assertRaises(AssertionError) as excinfo:
+ TestSerializer().fields
+ expected = (
+ "The field 'missing' was declared on serializer TestSerializer, "
+ "but has not been included in the 'fields' option."
+ )
+ assert str(excinfo.exception) == expected
+
+ def test_missing_superclass_field(self):
+ """
+ Fields that have been declared on a parent of the serializer class may
+ be excluded from the `Meta.fields` option.
+ """
+ class TestSerializer(serializers.ModelSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+
+ class ChildSerializer(TestSerializer):
+ missing = serializers.ReadOnlyField()
+
+ class Meta:
+ model = RegularFieldsModel
+ fields = ('auto_field',)
+
+ ChildSerializer().fields
+
+
+# Tests for relational field mappings.
+# ------------------------------------
+
+class ForeignKeyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ManyToManyTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class OneToOneTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class ThroughTargetModel(models.Model):
+ name = models.CharField(max_length=100)
+
+
+class Supplementary(models.Model):
+ extra = models.IntegerField()
+ forwards = models.ForeignKey('ThroughTargetModel')
+ backwards = models.ForeignKey('RelationalModel')
+
+
+class RelationalModel(models.Model):
+ foreign_key = models.ForeignKey(ForeignKeyTargetModel, related_name='reverse_foreign_key')
+ many_to_many = models.ManyToManyField(ManyToManyTargetModel, related_name='reverse_many_to_many')
+ one_to_one = models.OneToOneField(OneToOneTargetModel, related_name='reverse_one_to_one')
+ through = models.ManyToManyField(ThroughTargetModel, through=Supplementary, related_name='reverse_through')
+
+
+class TestRelationalFieldMappings(TestCase):
+ def test_pk_relations(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = PrimaryKeyRelatedField(queryset=ForeignKeyTargetModel.objects.all())
+ one_to_one = PrimaryKeyRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>])
+ many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all())
+ through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_nested_relations(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ foreign_key = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_hyperlinked_relations(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = HyperlinkedRelatedField(queryset=ForeignKeyTargetModel.objects.all(), view_name='foreignkeytargetmodel-detail')
+ one_to_one = HyperlinkedRelatedField(queryset=OneToOneTargetModel.objects.all(), validators=[<UniqueValidator(queryset=RelationalModel.objects.all())>], view_name='onetoonetargetmodel-detail')
+ many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail')
+ through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail')
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_nested_hyperlinked_relations(self):
+ class TestSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = RelationalModel
+ depth = 1
+
+ expected = dedent("""
+ TestSerializer():
+ url = HyperlinkedIdentityField(view_name='relationalmodel-detail')
+ foreign_key = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='foreignkeytargetmodel-detail')
+ name = CharField(max_length=100)
+ one_to_one = NestedSerializer(read_only=True):
+ url = HyperlinkedIdentityField(view_name='onetoonetargetmodel-detail')
+ name = CharField(max_length=100)
+ many_to_many = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='manytomanytargetmodel-detail')
+ name = CharField(max_length=100)
+ through = NestedSerializer(many=True, read_only=True):
+ url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail')
+ name = CharField(max_length=100)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_foreign_key(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeyTargetModel
+ fields = ('id', 'name', 'reverse_foreign_key')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_one_to_one(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = OneToOneTargetModel
+ fields = ('id', 'name', 'reverse_one_to_one')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_many_to_many(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ManyToManyTargetModel
+ fields = ('id', 'name', 'reverse_many_to_many')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all())
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+ def test_pk_reverse_through(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ThroughTargetModel
+ fields = ('id', 'name', 'reverse_through')
+
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ name = CharField(max_length=100)
+ reverse_through = PrimaryKeyRelatedField(many=True, read_only=True)
+ """)
+ self.assertEqual(unicode_repr(TestSerializer()), expected)
+
+
+class TestIntegration(TestCase):
+ def setUp(self):
+ self.foreign_key_target = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ self.one_to_one_target = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ self.many_to_many_targets = [
+ ManyToManyTargetModel.objects.create(
+ name='many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ self.instance = RelationalModel.objects.create(
+ foreign_key=self.foreign_key_target,
+ one_to_one=self.one_to_one_target,
+ )
+ self.instance.many_to_many = self.many_to_many_targets
+ self.instance.save()
+
+ def test_pk_retrival(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ serializer = TestSerializer(self.instance)
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': self.foreign_key_target.pk,
+ 'one_to_one': self.one_to_one_target.pk,
+ 'many_to_many': [item.pk for item in self.many_to_many_targets],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+ def test_pk_create(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = TestSerializer(data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+ def test_pk_update(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = RelationalModel
+
+ new_foreign_key = ForeignKeyTargetModel.objects.create(
+ name='foreign_key'
+ )
+ new_one_to_one = OneToOneTargetModel.objects.create(
+ name='one_to_one'
+ )
+ new_many_to_many = [
+ ManyToManyTargetModel.objects.create(
+ name='new many_to_many (%d)' % idx
+ ) for idx in range(3)
+ ]
+ data = {
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ }
+
+ # Serializer should validate okay.
+ serializer = TestSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+
+ # Creating the instance, relationship attributes should be set.
+ instance = serializer.save()
+ assert instance.foreign_key.pk == new_foreign_key.pk
+ assert instance.one_to_one.pk == new_one_to_one.pk
+ assert [
+ item.pk for item in instance.many_to_many.all()
+ ] == [
+ item.pk for item in new_many_to_many
+ ]
+ assert list(instance.through.all()) == []
+
+ # Representation should be correct.
+ expected = {
+ 'id': self.instance.pk,
+ 'foreign_key': new_foreign_key.pk,
+ 'one_to_one': new_one_to_one.pk,
+ 'many_to_many': [item.pk for item in new_many_to_many],
+ 'through': []
+ }
+ self.assertEqual(serializer.data, expected)
+
+
+# Tests for bulk create using `ListSerializer`.
+
+class BulkCreateModel(models.Model):
+ name = models.CharField(max_length=10)
+
+
+class TestBulkCreate(TestCase):
+ def test_bulk_create(self):
+ class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BulkCreateModel
+ fields = ('name',)
+
+ class BulkCreateSerializer(serializers.ListSerializer):
+ child = BasicModelSerializer()
+
+ data = [{'name': 'a'}, {'name': 'b'}, {'name': 'c'}]
+ serializer = BulkCreateSerializer(data=data)
+ assert serializer.is_valid()
+
+ # Objects are returned by save().
+ instances = serializer.save()
+ assert len(instances) == 3
+ assert [item.name for item in instances] == ['a', 'b', 'c']
+
+ # Objects have been created in the database.
+ assert BulkCreateModel.objects.count() == 3
+ assert list(BulkCreateModel.objects.values_list('name', flat=True)) == ['a', 'b', 'c']
+
+ # Serializer returns correct data.
+ assert serializer.data == data
+
+
+class TestMetaClassModel(models.Model):
+ text = models.CharField(max_length=100)
+
+
+class TestSerializerMetaClass(TestCase):
+ def test_meta_class_fields_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `fields` option must be a list or tuple"
+ )
+
+ def test_meta_class_exclude_option(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ exclude = 'text'
+
+ with self.assertRaises(TypeError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ assert str(exception).startswith(
+ "The `exclude` option must be a list or tuple"
+ )
+
+ def test_meta_class_fields_and_exclude_options(self):
+ class ExampleSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = TestMetaClassModel
+ fields = ('text',)
+ exclude = ('text',)
+
+ with self.assertRaises(AssertionError) as result:
+ ExampleSerializer().fields
+
+ exception = result.exception
+ self.assertEqual(
+ str(exception),
+ "Cannot set both 'fields' and 'exclude' options on serializer ExampleSerializer."
+ )
diff --git a/rest_framework/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py
index 00c15327..15627e1d 100644
--- a/rest_framework/tests/test_multitable_inheritance.py
+++ b/tests/test_multitable_inheritance.py
@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import RESTFrameworkModel
+from tests.models import RESTFrameworkModel
# Models
@@ -31,7 +31,7 @@ class AssociatedModelSerializer(serializers.ModelSerializer):
# Tests
-class IneritedModelSerializationTests(TestCase):
+class InheritedModelSerializationTests(TestCase):
def test_multitable_inherited_model_fields_as_expected(self):
"""
@@ -48,8 +48,8 @@ class IneritedModelSerializationTests(TestCase):
Assert that a model with a onetoone field that is the primary key is
not treated like a derived model
"""
- parent = ParentModel(name1='parent name')
- associate = AssociatedModel(name='hello', ref=parent)
+ parent = ParentModel.objects.create(name1='parent name')
+ associate = AssociatedModel.objects.create(name='hello', ref=parent)
serializer = AssociatedModelSerializer(associate)
self.assertEqual(set(serializer.data.keys()),
set(['name', 'ref']))
diff --git a/rest_framework/tests/test_negotiation.py b/tests/test_negotiation.py
index 04b89eb6..04b89eb6 100644
--- a/rest_framework/tests/test_negotiation.py
+++ b/tests/test_negotiation.py
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
new file mode 100644
index 00000000..6b39a6f2
--- /dev/null
+++ b/tests/test_pagination.py
@@ -0,0 +1,671 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from rest_framework import exceptions, generics, pagination, serializers, status, filters
+from rest_framework.request import Request
+from rest_framework.pagination import PageLink, PAGE_BREAK
+from rest_framework.test import APIRequestFactory
+import pytest
+
+factory = APIRequestFactory()
+
+
+class TestPaginationIntegration:
+ """
+ Integration tests.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ class EvenItemsOnly(filters.BaseFilterBackend):
+ def filter_queryset(self, request, queryset, view):
+ return [item for item in queryset if item % 2 == 0]
+
+ class BasicPagination(pagination.PageNumberPagination):
+ page_size = 5
+ page_size_query_param = 'page_size'
+ max_page_size = 20
+
+ self.view = generics.ListAPIView.as_view(
+ serializer_class=PassThroughSerializer,
+ queryset=range(1, 101),
+ filter_backends=[EvenItemsOnly],
+ pagination_class=BasicPagination
+ )
+
+ def test_filtered_items_are_paginated(self):
+ request = factory.get('/', {'page': 2})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [12, 14, 16, 18, 20],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page=3',
+ 'count': 50
+ }
+
+ def test_setting_page_size(self):
+ """
+ When 'paginate_by_param' is set, the client may choose a page size.
+ """
+ request = factory.get('/', {'page_size': 10})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [2, 4, 6, 8, 10, 12, 14, 16, 18, 20],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=10',
+ 'count': 50
+ }
+
+ def test_setting_page_size_over_maximum(self):
+ """
+ When page_size parameter exceeds maxiumum allowable,
+ then it should be capped to the maxiumum.
+ """
+ request = factory.get('/', {'page_size': 1000})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [
+ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20,
+ 22, 24, 26, 28, 30, 32, 34, 36, 38, 40
+ ],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=1000',
+ 'count': 50
+ }
+
+ def test_setting_page_size_to_zero(self):
+ """
+ When page_size parameter is invalid it should return to the default.
+ """
+ request = factory.get('/', {'page_size': 0})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [2, 4, 6, 8, 10],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2&page_size=0',
+ 'count': 50
+ }
+
+ def test_additional_query_params_are_preserved(self):
+ request = factory.get('/', {'page': 2, 'filter': 'even'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [12, 14, 16, 18, 20],
+ 'previous': 'http://testserver/?filter=even',
+ 'next': 'http://testserver/?filter=even&page=3',
+ 'count': 50
+ }
+
+ def test_404_not_found_for_zero_page(self):
+ request = factory.get('/', {'page': '0'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert response.data == {
+ 'detail': 'Invalid page "0": That page number is less than 1.'
+ }
+
+ def test_404_not_found_for_invalid_page(self):
+ request = factory.get('/', {'page': 'invalid'})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ assert response.data == {
+ 'detail': 'Invalid page "invalid": That page number is not an integer.'
+ }
+
+
+class TestPaginationDisabledIntegration:
+ """
+ Integration tests for disabled pagination.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ self.view = generics.ListAPIView.as_view(
+ serializer_class=PassThroughSerializer,
+ queryset=range(1, 101),
+ pagination_class=None
+ )
+
+ def test_unpaginated_list(self):
+ request = factory.get('/', {'page': 2})
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == list(range(1, 101))
+
+
+class TestDeprecatedStylePagination:
+ """
+ Integration tests for deprecated style of setting pagination
+ attributes on the view.
+ """
+
+ def setup(self):
+ class PassThroughSerializer(serializers.BaseSerializer):
+ def to_representation(self, item):
+ return item
+
+ class ExampleView(generics.ListAPIView):
+ serializer_class = PassThroughSerializer
+ queryset = range(1, 101)
+ pagination_class = pagination.PageNumberPagination
+ paginate_by = 20
+ page_query_param = 'page_number'
+
+ self.view = ExampleView.as_view()
+
+ def test_paginate_by_attribute_on_view(self):
+ request = factory.get('/?page_number=2')
+ response = self.view(request)
+ assert response.status_code == status.HTTP_200_OK
+ assert response.data == {
+ 'results': [
+ 21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
+ 31, 32, 33, 34, 35, 36, 37, 38, 39, 40
+ ],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page_number=3',
+ 'count': 100
+ }
+
+
+class TestPageNumberPagination:
+ """
+ Unit tests for `pagination.PageNumberPagination`.
+ """
+
+ def setup(self):
+ class ExamplePagination(pagination.PageNumberPagination):
+ page_size = 5
+ self.pagination = ExamplePagination()
+ self.queryset = range(1, 101)
+
+ def paginate_queryset(self, request):
+ return list(self.pagination.paginate_queryset(self.queryset, request))
+
+ def get_paginated_content(self, queryset):
+ response = self.pagination.get_paginated_response(queryset)
+ return response.data
+
+ def get_html_context(self):
+ return self.pagination.get_html_context()
+
+ def test_no_page_number(self):
+ request = Request(factory.get('/'))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [1, 2, 3, 4, 5]
+ assert content == {
+ 'results': [1, 2, 3, 4, 5],
+ 'previous': None,
+ 'next': 'http://testserver/?page=2',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': None,
+ 'next_url': 'http://testserver/?page=2',
+ 'page_links': [
+ PageLink('http://testserver/', 1, True, False),
+ PageLink('http://testserver/?page=2', 2, False, False),
+ PageLink('http://testserver/?page=3', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=20', 20, False, False),
+ ]
+ }
+ assert self.pagination.display_page_controls
+ assert isinstance(self.pagination.to_html(), type(''))
+
+ def test_second_page(self):
+ request = Request(factory.get('/', {'page': 2}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [6, 7, 8, 9, 10]
+ assert content == {
+ 'results': [6, 7, 8, 9, 10],
+ 'previous': 'http://testserver/',
+ 'next': 'http://testserver/?page=3',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/',
+ 'next_url': 'http://testserver/?page=3',
+ 'page_links': [
+ PageLink('http://testserver/', 1, False, False),
+ PageLink('http://testserver/?page=2', 2, True, False),
+ PageLink('http://testserver/?page=3', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=20', 20, False, False),
+ ]
+ }
+
+ def test_last_page(self):
+ request = Request(factory.get('/', {'page': 'last'}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [96, 97, 98, 99, 100]
+ assert content == {
+ 'results': [96, 97, 98, 99, 100],
+ 'previous': 'http://testserver/?page=19',
+ 'next': None,
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?page=19',
+ 'next_url': None,
+ 'page_links': [
+ PageLink('http://testserver/', 1, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?page=18', 18, False, False),
+ PageLink('http://testserver/?page=19', 19, False, False),
+ PageLink('http://testserver/?page=20', 20, True, False),
+ ]
+ }
+
+ def test_invalid_page(self):
+ request = Request(factory.get('/', {'page': 'invalid'}))
+ with pytest.raises(exceptions.NotFound):
+ self.paginate_queryset(request)
+
+
+class TestLimitOffset:
+ """
+ Unit tests for `pagination.LimitOffsetPagination`.
+ """
+
+ def setup(self):
+ class ExamplePagination(pagination.LimitOffsetPagination):
+ default_limit = 10
+ self.pagination = ExamplePagination()
+ self.queryset = range(1, 101)
+
+ def paginate_queryset(self, request):
+ return list(self.pagination.paginate_queryset(self.queryset, request))
+
+ def get_paginated_content(self, queryset):
+ response = self.pagination.get_paginated_response(queryset)
+ return response.data
+
+ def get_html_context(self):
+ return self.pagination.get_html_context()
+
+ def test_no_offset(self):
+ request = Request(factory.get('/', {'limit': 5}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [1, 2, 3, 4, 5]
+ assert content == {
+ 'results': [1, 2, 3, 4, 5],
+ 'previous': None,
+ 'next': 'http://testserver/?limit=5&offset=5',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': None,
+ 'next_url': 'http://testserver/?limit=5&offset=5',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, True, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, False, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+ assert self.pagination.display_page_controls
+ assert isinstance(self.pagination.to_html(), type(''))
+
+ def test_single_offset(self):
+ """
+ When the offset is not a multiple of the limit we get some edge cases:
+ * The first page should still be offset zero.
+ * We may end up displaying an extra page in the pagination control.
+ """
+ request = Request(factory.get('/', {'limit': 5, 'offset': 1}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [2, 3, 4, 5, 6]
+ assert content == {
+ 'results': [2, 3, 4, 5, 6],
+ 'previous': 'http://testserver/?limit=5',
+ 'next': 'http://testserver/?limit=5&offset=6',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5',
+ 'next_url': 'http://testserver/?limit=5&offset=6',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=1', 2, True, False),
+ PageLink('http://testserver/?limit=5&offset=6', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=96', 21, False, False),
+ ]
+ }
+
+ def test_first_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 5}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [6, 7, 8, 9, 10]
+ assert content == {
+ 'results': [6, 7, 8, 9, 10],
+ 'previous': 'http://testserver/?limit=5',
+ 'next': 'http://testserver/?limit=5&offset=10',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5',
+ 'next_url': 'http://testserver/?limit=5&offset=10',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, True, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+
+ def test_middle_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 10}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [11, 12, 13, 14, 15]
+ assert content == {
+ 'results': [11, 12, 13, 14, 15],
+ 'previous': 'http://testserver/?limit=5&offset=5',
+ 'next': 'http://testserver/?limit=5&offset=15',
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5&offset=5',
+ 'next_url': 'http://testserver/?limit=5&offset=15',
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PageLink('http://testserver/?limit=5&offset=5', 2, False, False),
+ PageLink('http://testserver/?limit=5&offset=10', 3, True, False),
+ PageLink('http://testserver/?limit=5&offset=15', 4, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=95', 20, False, False),
+ ]
+ }
+
+ def test_ending_offset(self):
+ request = Request(factory.get('/', {'limit': 5, 'offset': 95}))
+ queryset = self.paginate_queryset(request)
+ content = self.get_paginated_content(queryset)
+ context = self.get_html_context()
+ assert queryset == [96, 97, 98, 99, 100]
+ assert content == {
+ 'results': [96, 97, 98, 99, 100],
+ 'previous': 'http://testserver/?limit=5&offset=90',
+ 'next': None,
+ 'count': 100
+ }
+ assert context == {
+ 'previous_url': 'http://testserver/?limit=5&offset=90',
+ 'next_url': None,
+ 'page_links': [
+ PageLink('http://testserver/?limit=5', 1, False, False),
+ PAGE_BREAK,
+ PageLink('http://testserver/?limit=5&offset=85', 18, False, False),
+ PageLink('http://testserver/?limit=5&offset=90', 19, False, False),
+ PageLink('http://testserver/?limit=5&offset=95', 20, True, False),
+ ]
+ }
+
+ def test_invalid_offset(self):
+ """
+ An invalid offset query param should be treated as 0.
+ """
+ request = Request(factory.get('/', {'limit': 5, 'offset': 'invalid'}))
+ queryset = self.paginate_queryset(request)
+ assert queryset == [1, 2, 3, 4, 5]
+
+ def test_invalid_limit(self):
+ """
+ An invalid limit query param should be ignored in favor of the default.
+ """
+ request = Request(factory.get('/', {'limit': 'invalid', 'offset': 0}))
+ queryset = self.paginate_queryset(request)
+ assert queryset == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+
+class TestCursorPagination:
+ """
+ Unit tests for `pagination.CursorPagination`.
+ """
+
+ def setup(self):
+ class MockObject(object):
+ def __init__(self, idx):
+ self.created = idx
+
+ class MockQuerySet(object):
+ def __init__(self, items):
+ self.items = items
+
+ def filter(self, created__gt=None, created__lt=None):
+ if created__gt is not None:
+ return MockQuerySet([
+ item for item in self.items
+ if item.created > int(created__gt)
+ ])
+
+ assert created__lt is not None
+ return MockQuerySet([
+ item for item in self.items
+ if item.created < int(created__lt)
+ ])
+
+ def order_by(self, *ordering):
+ if ordering[0].startswith('-'):
+ return MockQuerySet(list(reversed(self.items)))
+ return self
+
+ def __getitem__(self, sliced):
+ return self.items[sliced]
+
+ class ExamplePagination(pagination.CursorPagination):
+ page_size = 5
+ ordering = 'created'
+
+ self.pagination = ExamplePagination()
+ self.queryset = MockQuerySet([
+ MockObject(idx) for idx in [
+ 1, 1, 1, 1, 1,
+ 1, 2, 3, 4, 4,
+ 4, 4, 5, 6, 7,
+ 7, 7, 7, 7, 7,
+ 7, 7, 7, 8, 9,
+ 9, 9, 9, 9, 9
+ ]
+ ])
+
+ def get_pages(self, url):
+ """
+ Given a URL return a tuple of:
+
+ (previous page, current page, next page, previous url, next url)
+ """
+ request = Request(factory.get(url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ current = [item.created for item in queryset]
+
+ next_url = self.pagination.get_next_link()
+ previous_url = self.pagination.get_previous_link()
+
+ if next_url is not None:
+ request = Request(factory.get(next_url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ next = [item.created for item in queryset]
+ else:
+ next = None
+
+ if previous_url is not None:
+ request = Request(factory.get(previous_url))
+ queryset = self.pagination.paginate_queryset(self.queryset, request)
+ previous = [item.created for item in queryset]
+ else:
+ previous = None
+
+ return (previous, current, next, previous_url, next_url)
+
+ def test_invalid_cursor(self):
+ request = Request(factory.get('/', {'cursor': '123'}))
+ with pytest.raises(exceptions.NotFound):
+ self.pagination.paginate_queryset(self.queryset, request)
+
+ def test_use_with_ordering_filter(self):
+ class MockView:
+ filter_backends = (filters.OrderingFilter,)
+ ordering_fields = ['username', 'created']
+ ordering = 'created'
+
+ request = Request(factory.get('/', {'ordering': 'username'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('username',)
+
+ request = Request(factory.get('/', {'ordering': '-username'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('-username',)
+
+ request = Request(factory.get('/', {'ordering': 'invalid'}))
+ ordering = self.pagination.get_ordering(request, [], MockView())
+ assert ordering == ('created',)
+
+ def test_cursor_pagination(self):
+ (previous, current, next, previous_url, next_url) = self.get_pages('/')
+
+ assert previous is None
+ assert current == [1, 1, 1, 1, 1]
+ assert next == [1, 2, 3, 4, 4]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [1, 1, 1, 1, 1]
+ assert current == [1, 2, 3, 4, 4]
+ assert next == [4, 4, 5, 6, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [1, 2, 3, 4, 4]
+ assert current == [4, 4, 5, 6, 7]
+ assert next == [7, 7, 7, 7, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [4, 4, 4, 5, 6] # Paging artifact
+ assert current == [7, 7, 7, 7, 7]
+ assert next == [7, 7, 7, 8, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [7, 7, 7, 7, 7]
+ assert current == [7, 7, 7, 8, 9]
+ assert next == [9, 9, 9, 9, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(next_url)
+
+ assert previous == [7, 7, 7, 8, 9]
+ assert current == [9, 9, 9, 9, 9]
+ assert next is None
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [7, 7, 7, 7, 7]
+ assert current == [7, 7, 7, 8, 9]
+ assert next == [9, 9, 9, 9, 9]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [4, 4, 5, 6, 7]
+ assert current == [7, 7, 7, 7, 7]
+ assert next == [8, 9, 9, 9, 9] # Paging artifact
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [1, 2, 3, 4, 4]
+ assert current == [4, 4, 5, 6, 7]
+ assert next == [7, 7, 7, 7, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous == [1, 1, 1, 1, 1]
+ assert current == [1, 2, 3, 4, 4]
+ assert next == [4, 4, 5, 6, 7]
+
+ (previous, current, next, previous_url, next_url) = self.get_pages(previous_url)
+
+ assert previous is None
+ assert current == [1, 1, 1, 1, 1]
+ assert next == [1, 2, 3, 4, 4]
+
+ assert isinstance(self.pagination.to_html(), type(''))
+
+
+def test_get_displayed_page_numbers():
+ """
+ Test our contextual page display function.
+
+ This determines which pages to display in a pagination control,
+ given the current page and the last page.
+ """
+ displayed_page_numbers = pagination._get_displayed_page_numbers
+
+ # At five pages or less, all pages are displayed, always.
+ assert displayed_page_numbers(1, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(2, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(3, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(4, 5) == [1, 2, 3, 4, 5]
+ assert displayed_page_numbers(5, 5) == [1, 2, 3, 4, 5]
+
+ # Between six and either pages we may have a single page break.
+ assert displayed_page_numbers(1, 6) == [1, 2, 3, None, 6]
+ assert displayed_page_numbers(2, 6) == [1, 2, 3, None, 6]
+ assert displayed_page_numbers(3, 6) == [1, 2, 3, 4, 5, 6]
+ assert displayed_page_numbers(4, 6) == [1, 2, 3, 4, 5, 6]
+ assert displayed_page_numbers(5, 6) == [1, None, 4, 5, 6]
+ assert displayed_page_numbers(6, 6) == [1, None, 4, 5, 6]
+
+ assert displayed_page_numbers(1, 7) == [1, 2, 3, None, 7]
+ assert displayed_page_numbers(2, 7) == [1, 2, 3, None, 7]
+ assert displayed_page_numbers(3, 7) == [1, 2, 3, 4, None, 7]
+ assert displayed_page_numbers(4, 7) == [1, 2, 3, 4, 5, 6, 7]
+ assert displayed_page_numbers(5, 7) == [1, None, 4, 5, 6, 7]
+ assert displayed_page_numbers(6, 7) == [1, None, 5, 6, 7]
+ assert displayed_page_numbers(7, 7) == [1, None, 5, 6, 7]
+
+ assert displayed_page_numbers(1, 8) == [1, 2, 3, None, 8]
+ assert displayed_page_numbers(2, 8) == [1, 2, 3, None, 8]
+ assert displayed_page_numbers(3, 8) == [1, 2, 3, 4, None, 8]
+ assert displayed_page_numbers(4, 8) == [1, 2, 3, 4, 5, None, 8]
+ assert displayed_page_numbers(5, 8) == [1, None, 4, 5, 6, 7, 8]
+ assert displayed_page_numbers(6, 8) == [1, None, 5, 6, 7, 8]
+ assert displayed_page_numbers(7, 8) == [1, None, 6, 7, 8]
+ assert displayed_page_numbers(8, 8) == [1, None, 6, 7, 8]
+
+ # At nine or more pages we may have two page breaks, one on each side.
+ assert displayed_page_numbers(1, 9) == [1, 2, 3, None, 9]
+ assert displayed_page_numbers(2, 9) == [1, 2, 3, None, 9]
+ assert displayed_page_numbers(3, 9) == [1, 2, 3, 4, None, 9]
+ assert displayed_page_numbers(4, 9) == [1, 2, 3, 4, 5, None, 9]
+ assert displayed_page_numbers(5, 9) == [1, None, 4, 5, 6, None, 9]
+ assert displayed_page_numbers(6, 9) == [1, None, 5, 6, 7, 8, 9]
+ assert displayed_page_numbers(7, 9) == [1, None, 6, 7, 8, 9]
+ assert displayed_page_numbers(8, 9) == [1, None, 7, 8, 9]
+ assert displayed_page_numbers(9, 9) == [1, None, 7, 8, 9]
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
new file mode 100644
index 00000000..fe6aec19
--- /dev/null
+++ b/tests/test_parsers.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals
+from django import forms
+from django.core.files.uploadhandler import MemoryFileUploadHandler
+from django.test import TestCase
+from django.utils.six.moves import StringIO
+from rest_framework.exceptions import ParseError
+from rest_framework.parsers import FormParser, FileUploadParser
+
+
+class Form(forms.Form):
+ field1 = forms.CharField(max_length=3)
+ field2 = forms.CharField()
+
+
+class TestFormParser(TestCase):
+ def setUp(self):
+ self.string = "field1=abc&field2=defghijk"
+
+ def test_parse(self):
+ """ Make sure the `QueryDict` works OK """
+ parser = FormParser()
+
+ stream = StringIO(self.string)
+ data = parser.parse(stream)
+
+ self.assertEqual(Form(data).is_valid(), True)
+
+
+class TestFileUploadParser(TestCase):
+ def setUp(self):
+ class MockRequest(object):
+ pass
+ from io import BytesIO
+ self.stream = BytesIO(
+ "Test text file".encode('utf-8')
+ )
+ request = MockRequest()
+ request.upload_handlers = (MemoryFileUploadHandler(),)
+ request.META = {
+ 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt',
+ 'HTTP_CONTENT_LENGTH': 14,
+ }
+ self.parser_context = {'request': request, 'kwargs': {}}
+
+ def test_parse(self):
+ """
+ Parse raw file upload.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ data_and_files = parser.parse(self.stream, None, self.parser_context)
+ file_obj = data_and_files.files['file']
+ self.assertEqual(file_obj._size, 14)
+
+ def test_parse_missing_filename(self):
+ """
+ Parse raw file upload when filename is missing.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
+ with self.assertRaises(ParseError):
+ parser.parse(self.stream, None, self.parser_context)
+
+ def test_parse_missing_filename_multiple_upload_handlers(self):
+ """
+ Parse raw file upload with multiple handlers when filename is missing.
+ Regression test for #2109.
+ """
+ parser = FileUploadParser()
+ self.stream.seek(0)
+ self.parser_context['request'].upload_handlers = (
+ MemoryFileUploadHandler(),
+ MemoryFileUploadHandler()
+ )
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = ''
+ with self.assertRaises(ParseError):
+ parser.parse(self.stream, None, self.parser_context)
+
+ def test_get_filename(self):
+ parser = FileUploadParser()
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'file.txt')
+
+ def test_get_encoded_filename(self):
+ parser = FileUploadParser()
+
+ self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ def __replace_content_disposition(self, disposition):
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
new file mode 100644
index 00000000..97bac33d
--- /dev/null
+++ b/tests/test_permissions.py
@@ -0,0 +1,312 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User, Permission, Group
+from django.db import models
+from django.test import TestCase
+from django.utils import unittest
+from rest_framework import generics, serializers, status, permissions, authentication, HTTP_HEADER_ENCODING
+from rest_framework.compat import guardian, get_model_name
+from rest_framework.filters import DjangoObjectPermissionsFilter
+from rest_framework.test import APIRequestFactory
+from tests.models import BasicModel
+import base64
+
+factory = APIRequestFactory()
+
+
+class BasicSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
+
+
+class RootView(generics.ListCreateAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [permissions.DjangoModelPermissions]
+
+
+class InstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicModel.objects.all()
+ serializer_class = BasicSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [permissions.DjangoModelPermissions]
+
+root_view = RootView.as_view()
+instance_view = InstanceView.as_view()
+
+
+def basic_auth_header(username, password):
+ credentials = ('%s:%s' % (username, password))
+ base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
+ return 'Basic %s' % base64_credentials
+
+
+class ModelPermissionsIntegrationTests(TestCase):
+ def setUp(self):
+ User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
+ user = User.objects.create_user('permitted', 'permitted@example.com', 'password')
+ user.user_permissions = [
+ Permission.objects.get(codename='add_basicmodel'),
+ Permission.objects.get(codename='change_basicmodel'),
+ Permission.objects.get(codename='delete_basicmodel')
+ ]
+ user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
+ user.user_permissions = [
+ Permission.objects.get(codename='change_basicmodel'),
+ ]
+
+ self.permitted_credentials = basic_auth_header('permitted', 'password')
+ self.disallowed_credentials = basic_auth_header('disallowed', 'password')
+ self.updateonly_credentials = basic_auth_header('updateonly', 'password')
+
+ BasicModel(text='foo').save()
+
+ def test_has_create_permissions(self):
+ request = factory.post('/', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = root_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_has_put_permissions(self):
+ request = factory.put('/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_has_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
+ response = instance_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_does_not_have_create_permissions(self):
+ request = factory.post('/', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = root_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_does_not_have_put_permissions(self):
+ request = factory.put('/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_does_not_have_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
+ response = instance_view(request, pk=1)
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ def test_options_permitted(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['POST'])
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+
+ def test_options_disallowed(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ def test_options_updateonly(self):
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
+ response = root_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertNotIn('actions', response.data)
+
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
+ response = instance_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertIn('actions', response.data)
+ self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
+
+
+class BasicPermModel(models.Model):
+ text = models.CharField(max_length=100)
+
+ class Meta:
+ app_label = 'tests'
+ permissions = (
+ ('view_basicpermmodel', 'Can view basic perm model'),
+ # add, change, delete built in to django
+ )
+
+
+class BasicPermSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicPermModel
+
+
+# Custom object-level permission, that includes 'view' permissions
+class ViewObjectPermissions(permissions.DjangoObjectPermissions):
+ perms_map = {
+ 'GET': ['%(app_label)s.view_%(model_name)s'],
+ 'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
+ 'HEAD': ['%(app_label)s.view_%(model_name)s'],
+ 'POST': ['%(app_label)s.add_%(model_name)s'],
+ 'PUT': ['%(app_label)s.change_%(model_name)s'],
+ 'PATCH': ['%(app_label)s.change_%(model_name)s'],
+ 'DELETE': ['%(app_label)s.delete_%(model_name)s'],
+ }
+
+
+class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [ViewObjectPermissions]
+
+object_permissions_view = ObjectPermissionInstanceView.as_view()
+
+
+class ObjectPermissionListView(generics.ListAPIView):
+ queryset = BasicPermModel.objects.all()
+ serializer_class = BasicPermSerializer
+ authentication_classes = [authentication.BasicAuthentication]
+ permission_classes = [ViewObjectPermissions]
+
+object_permissions_list_view = ObjectPermissionListView.as_view()
+
+
+@unittest.skipUnless(guardian, 'django-guardian not installed')
+class ObjectPermissionsIntegrationTests(TestCase):
+ """
+ Integration tests for the object level permissions API.
+ """
+ def setUp(self):
+ from guardian.shortcuts import assign_perm
+
+ # create users
+ create = User.objects.create_user
+ users = {
+ 'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'),
+ 'readonly': create('readonly', 'readonly@example.com', 'password'),
+ 'writeonly': create('writeonly', 'writeonly@example.com', 'password'),
+ 'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'),
+ }
+
+ # give everyone model level permissions, as we are not testing those
+ everyone = Group.objects.create(name='everyone')
+ model_name = get_model_name(BasicPermModel)
+ app_label = BasicPermModel._meta.app_label
+ f = '{0}_{1}'.format
+ perms = {
+ 'view': f('view', model_name),
+ 'change': f('change', model_name),
+ 'delete': f('delete', model_name)
+ }
+ for perm in perms.values():
+ perm = '{0}.{1}'.format(app_label, perm)
+ assign_perm(perm, everyone)
+ everyone.user_set.add(*users.values())
+
+ # appropriate object level permissions
+ readers = Group.objects.create(name='readers')
+ writers = Group.objects.create(name='writers')
+ deleters = Group.objects.create(name='deleters')
+
+ model = BasicPermModel.objects.create(text='foo')
+
+ assign_perm(perms['view'], readers, model)
+ assign_perm(perms['change'], writers, model)
+ assign_perm(perms['delete'], deleters, model)
+
+ readers.user_set.add(users['fullaccess'], users['readonly'])
+ writers.user_set.add(users['fullaccess'], users['writeonly'])
+ deleters.user_set.add(users['fullaccess'], users['deleteonly'])
+
+ self.credentials = {}
+ for user in users.values():
+ self.credentials[user.username] = basic_auth_header(user.username, 'password')
+
+ # Delete
+ def test_can_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_cannot_delete_permissions(self):
+ request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+
+ # Update
+ def test_can_update_permissions(self):
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['writeonly']
+ )
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data.get('text'), 'foobar')
+
+ def test_cannot_update_permissions(self):
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ def test_cannot_update_permissions_non_existing(self):
+ request = factory.patch(
+ '/999', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
+ response = object_permissions_view(request, pk='999')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Read
+ def test_can_read_permissions(self):
+ request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_cannot_read_permissions(self):
+ request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly'])
+ response = object_permissions_view(request, pk='1')
+ self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
+
+ # Read list
+ def test_can_read_list_permissions(self):
+ request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
+ object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
+ response = object_permissions_list_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data[0].get('id'), 1)
+
+ def test_cannot_read_list_permissions(self):
+ request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['writeonly'])
+ object_permissions_list_view.cls.filter_backends = (DjangoObjectPermissionsFilter,)
+ response = object_permissions_list_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertListEqual(response.data, [])
diff --git a/tests/test_relations.py b/tests/test_relations.py
new file mode 100644
index 00000000..fbe176e2
--- /dev/null
+++ b/tests/test_relations.py
@@ -0,0 +1,169 @@
+from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.datastructures import MultiValueDict
+from rest_framework import serializers
+from rest_framework.fields import empty
+from rest_framework.test import APISimpleTestCase
+import pytest
+
+
+class TestStringRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.StringRelatedField()
+
+ def test_string_related_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == '<MockObject name=foo, pk=1>'
+
+
+class TestPrimaryKeyRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.PrimaryKeyRelatedField(queryset=self.queryset)
+
+ def test_pk_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.pk)
+ assert instance is self.instance
+
+ def test_pk_related_lookup_does_not_exist(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(4)
+ msg = excinfo.value.detail[0]
+ assert msg == 'Invalid pk "4" - object does not exist.'
+
+ def test_pk_related_lookup_invalid_type(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.detail[0]
+ assert msg == 'Incorrect type. Expected pk value, received BadType.'
+
+ def test_pk_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.pk
+
+
+class TestHyperlinkedIdentityField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example')
+ self.field.reverse = mock_reverse
+ self.field._context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_unsaved_object(self):
+ representation = self.field.to_representation(MockObject(pk=None))
+ assert representation is None
+
+ def test_representation_with_format(self):
+ self.field._context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.xml/'
+
+ def test_improperly_configured(self):
+ """
+ If a matching view cannot be reversed with the given instance,
+ the the user has misconfigured something, as the URL conf and the
+ hyperlinked field do not match.
+ """
+ self.field.reverse = fail_reverse
+ with pytest.raises(ImproperlyConfigured):
+ self.field.to_representation(self.instance)
+
+
+class TestHyperlinkedIdentityFieldWithFormat(APISimpleTestCase):
+ """
+ Tests for a hyperlinked identity field that has a `format` set,
+ which enforces that alternate formats are never linked too.
+
+ Eg. If your API includes some endpoints that accept both `.xml` and `.json`,
+ but other endpoints that only accept `.json`, we allow for hyperlinked
+ relationships that enforce only a single suffix type.
+ """
+
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.HyperlinkedIdentityField(view_name='example', format='json')
+ self.field.reverse = mock_reverse
+ self.field._context = {'request': True}
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1/'
+
+ def test_representation_with_format(self):
+ self.field._context['format'] = 'xml'
+ representation = self.field.to_representation(self.instance)
+ assert representation == 'http://example.org/example/1.json/'
+
+
+class TestSlugRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.queryset = MockQueryset([
+ MockObject(pk=1, name='foo'),
+ MockObject(pk=2, name='bar'),
+ MockObject(pk=3, name='baz')
+ ])
+ self.instance = self.queryset.items[2]
+ self.field = serializers.SlugRelatedField(
+ slug_field='name', queryset=self.queryset
+ )
+
+ def test_slug_related_lookup_exists(self):
+ instance = self.field.to_internal_value(self.instance.name)
+ assert instance is self.instance
+
+ def test_slug_related_lookup_does_not_exist(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value('doesnotexist')
+ msg = excinfo.value.detail[0]
+ assert msg == 'Object with name=doesnotexist does not exist.'
+
+ def test_slug_related_lookup_invalid_type(self):
+ with pytest.raises(serializers.ValidationError) as excinfo:
+ self.field.to_internal_value(BadType())
+ msg = excinfo.value.detail[0]
+ assert msg == 'Invalid value.'
+
+ def test_representation(self):
+ representation = self.field.to_representation(self.instance)
+ assert representation == self.instance.name
+
+
+class TestManyRelatedField(APISimpleTestCase):
+ def setUp(self):
+ self.instance = MockObject(pk=1, name='foo')
+ self.field = serializers.StringRelatedField(many=True)
+ self.field.field_name = 'foo'
+
+ def test_get_value_regular_dictionary_full(self):
+ assert 'bar' == self.field.get_value({'foo': 'bar'})
+ assert empty == self.field.get_value({'baz': 'bar'})
+
+ def test_get_value_regular_dictionary_partial(self):
+ setattr(self.field.root, 'partial', True)
+ assert 'bar' == self.field.get_value({'foo': 'bar'})
+ assert empty == self.field.get_value({'baz': 'bar'})
+
+ def test_get_value_multi_dictionary_full(self):
+ mvd = MultiValueDict({'foo': ['bar1', 'bar2']})
+ assert ['bar1', 'bar2'] == self.field.get_value(mvd)
+
+ mvd = MultiValueDict({'baz': ['bar1', 'bar2']})
+ assert [] == self.field.get_value(mvd)
+
+ def test_get_value_multi_dictionary_partial(self):
+ setattr(self.field.root, 'partial', True)
+ mvd = MultiValueDict({'foo': ['bar1', 'bar2']})
+ assert ['bar1', 'bar2'] == self.field.get_value(mvd)
+
+ mvd = MultiValueDict({'baz': ['bar1', 'bar2']})
+ assert empty == self.field.get_value(mvd)
diff --git a/rest_framework/tests/test_genericrelations.py b/tests/test_relations_generic.py
index c38bfb9f..b600b333 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/tests/test_relations_generic.py
@@ -3,9 +3,11 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericRelation, GenericForeignKey
from django.db import models
from django.test import TestCase
+from django.utils.encoding import python_2_unicode_compatible
from rest_framework import serializers
+@python_2_unicode_compatible
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
@@ -15,10 +17,11 @@ class Tag(models.Model):
object_id = models.PositiveIntegerField()
tagged_item = GenericForeignKey('content_type', 'object_id')
- def __unicode__(self):
+ def __str__(self):
return self.tag
+@python_2_unicode_compatible
class Bookmark(models.Model):
"""
A URL bookmark that may have multiple tags attached.
@@ -26,10 +29,11 @@ class Bookmark(models.Model):
url = models.URLField()
tags = GenericRelation(Tag)
- def __unicode__(self):
+ def __str__(self):
return 'Bookmark: %s' % self.url
+@python_2_unicode_compatible
class Note(models.Model):
"""
A textual note that may have multiple tags attached.
@@ -37,7 +41,7 @@ class Note(models.Model):
text = models.TextField()
tags = GenericRelation(Tag)
- def __unicode__(self):
+ def __str__(self):
return 'Note: %s' % self.text
@@ -56,11 +60,11 @@ class TestGenericRelations(TestCase):
"""
class BookmarkSerializer(serializers.ModelSerializer):
- tags = serializers.RelatedField(many=True)
+ tags = serializers.StringRelatedField(many=True)
class Meta:
model = Bookmark
- exclude = ('id',)
+ fields = ('tags', 'url')
serializer = BookmarkSerializer(self.bookmark)
expected = {
@@ -76,25 +80,25 @@ class TestGenericRelations(TestCase):
"""
class TagSerializer(serializers.ModelSerializer):
- tagged_item = serializers.RelatedField()
+ tagged_item = serializers.StringRelatedField()
class Meta:
model = Tag
- exclude = ('id', 'content_type', 'object_id')
+ fields = ('tag', 'tagged_item')
serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [
- {
- 'tag': 'django',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'python',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'reminder',
- 'tagged_item': 'Note: Remember the milk'
- }
+ {
+ 'tag': 'django',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'python',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'reminder',
+ 'tagged_item': 'Note: Remember the milk'
+ }
]
self.assertEqual(serializer.data, expected)
diff --git a/rest_framework/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
index 3c4d39af..33b09713 100644
--- a/rest_framework/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -1,10 +1,9 @@
from __future__ import unicode_literals
+from django.conf.urls import url
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.compat import patterns, url
from rest_framework.test import APIRequestFactory
-from rest_framework.tests.models import (
- BlogPost,
+from tests.models import (
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
)
@@ -16,7 +15,8 @@ request = factory.get('/') # Just to ensure we have a request in the serializer
def dummy_view(request, pk):
pass
-urlpatterns = patterns('',
+
+urlpatterns = [
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
@@ -25,7 +25,7 @@ urlpatterns = patterns('',
url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'),
url(r'^onetoonetarget/(?P<pk>[0-9]+)/$', dummy_view, name='onetoonetarget-detail'),
url(r'^nullableonetoonesource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableonetoonesource-detail'),
-)
+]
# ManyToMany
@@ -71,7 +71,7 @@ class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
# TODO: Add test that .data cannot be accessed prior to .is_valid
class HyperlinkedManyToManyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
for idx in range(1, 4):
@@ -86,11 +86,18 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_many_to_many_retrieve_prefetch_related(self):
+ queryset = ManyToManySource.objects.all().prefetch_related('targets')
+ serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
+ with self.assertNumQueries(2):
+ serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
@@ -100,7 +107,8 @@ class HyperlinkedManyToManyTests(TestCase):
{'url': 'http://testserver/manytomanytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/manytomanysource/2/', 'http://testserver/manytomanysource/3/']},
{'url': 'http://testserver/manytomanytarget/3/', 'name': 'target-3', 'sources': ['http://testserver/manytomanysource/3/']}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
def test_many_to_many_update(self):
data = {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
@@ -114,9 +122,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
self.assertEqual(serializer.data, expected)
@@ -179,7 +187,7 @@ class HyperlinkedManyToManyTests(TestCase):
class HyperlinkedForeignKeyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
@@ -198,7 +206,8 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeysource/2/', 'name': 'source-2', 'target': 'http://testserver/foreignkeytarget/1/'},
{'url': 'http://testserver/foreignkeysource/3/', 'name': 'source-3', 'target': 'http://testserver/foreignkeytarget/1/'}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(1):
+ self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -207,15 +216,16 @@ class HyperlinkedForeignKeyTests(TestCase):
{'url': 'http://testserver/foreignkeytarget/1/', 'name': 'target-1', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/2/', 'http://testserver/foreignkeysource/3/']},
{'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': []},
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(3):
+ self.assertEqual(serializer.data, expected)
def test_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeysource/1/', 'name': 'source-1', 'target': 'http://testserver/foreignkeytarget/2/'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -232,7 +242,7 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected url string, received int.']})
+ self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected URL string, received int.']})
def test_reverse_foreign_key_update(self):
data = {'url': 'http://testserver/foreignkeytarget/2/', 'name': 'target-2', 'sources': ['http://testserver/foreignkeysource/1/', 'http://testserver/foreignkeysource/3/']}
@@ -303,11 +313,11 @@ class HyperlinkedForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
class HyperlinkedNullableForeignKeyTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
@@ -376,8 +386,8 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -399,8 +409,8 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data, context={'request': request})
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -412,30 +422,9 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
class HyperlinkedNullableOneToOneTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
+ urls = 'tests.test_relations_hyperlink'
def setUp(self):
target = OneToOneTarget(name='target-1')
@@ -453,72 +442,3 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
]
self.assertEqual(serializer.data, expected)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-class HyperlinkedRelatedFieldSourceTests(TestCase):
- urls = 'rest_framework.tests.test_relations_hyperlink'
-
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_manager',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='get_blogposts_queryset',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.HyperlinkedRelatedField(
- many=True,
- source='a.b.c',
- view_name='dummy-url',
- )
- field.context = {'request': request}
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, ['http://testserver/dummyurl/1/'])
diff --git a/rest_framework/tests/test_relations_pk.py b/tests/test_relations_pk.py
index e2a1b815..ca43272b 100644
--- a/rest_framework/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -1,12 +1,11 @@
from __future__ import unicode_literals
-from django.db import models
from django.test import TestCase
+from django.utils import six
from rest_framework import serializers
-from rest_framework.tests.models import (
- BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
+from tests.models import (
+ ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
)
-from rest_framework.compat import six
# ManyToMany
@@ -65,11 +64,18 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_many_to_many_retrieve_prefetch_related(self):
+ queryset = ManyToManySource.objects.all().prefetch_related('targets')
+ serializer = ManyToManySourceSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
def test_reverse_many_to_many_retrieve(self):
queryset = ManyToManyTarget.objects.all()
@@ -79,7 +85,8 @@ class PKManyToManyTests(TestCase):
{'id': 2, 'name': 'target-2', 'sources': [2, 3]},
{'id': 3, 'name': 'target-3', 'sources': [3]}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
def test_many_to_many_update(self):
data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]}
@@ -93,9 +100,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
self.assertEqual(serializer.data, expected)
@@ -128,7 +135,6 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
- self.assertFalse(serializer.fields['targets'].read_only)
expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
@@ -137,10 +143,19 @@ class PKManyToManyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
+ def test_many_to_many_unsaved(self):
+ source = ManyToManySource(name='source-unsaved')
+
+ serializer = ManyToManySourceSerializer(source)
+
+ expected = {'id': None, 'name': 'source-unsaved', 'targets': []}
+ # no query if source hasn't been created yet
+ with self.assertNumQueries(0):
+ self.assertEqual(serializer.data, expected)
+
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
- self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(serializer.data, data)
@@ -176,7 +191,8 @@ class PKForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 1},
{'id': 3, 'name': 'source-3', 'target': 1}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(1):
+ self.assertEqual(serializer.data, expected)
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -185,15 +201,22 @@ class PKForeignKeyTests(TestCase):
{'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]},
{'id': 2, 'name': 'target-2', 'sources': []},
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(3):
+ self.assertEqual(serializer.data, expected)
+
+ def test_reverse_foreign_key_retrieve_prefetch_related(self):
+ queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
+ serializer = ForeignKeyTargetSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 2}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -210,7 +233,7 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
+ self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]})
def test_reverse_foreign_key_update(self):
data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]}
@@ -281,7 +304,26 @@ class PKForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
+
+ def test_foreign_key_with_unsaved(self):
+ source = ForeignKeySource(name='source-unsaved')
+ expected = {'id': None, 'name': 'source-unsaved', 'target': None}
+
+ serializer = ForeignKeySourceSerializer(source)
+
+ # no query if source hasn't been created yet
+ with self.assertNumQueries(0):
+ self.assertEqual(serializer.data, expected)
+
+ def test_foreign_key_with_empty(self):
+ """
+ Regression test for #1072
+
+ https://github.com/tomchristie/django-rest-framework/issues/1072
+ """
+ serializer = NullableForeignKeySourceSerializer()
+ self.assertEqual(serializer.data['target'], None)
class PKNullableForeignKeyTests(TestCase):
@@ -352,8 +394,8 @@ class PKNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -375,8 +417,8 @@ class PKNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -388,27 +430,6 @@ class PKNullableForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
- # reverse foreign keys MUST be read_only
- # In the general case they do not provide .remove() or .clear()
- # and cannot be arbitrarily set.
-
- # def test_reverse_foreign_key_update(self):
- # data = {'id': 1, 'name': 'target-1', 'sources': [1]}
- # instance = ForeignKeyTarget.objects.get(pk=1)
- # serializer = ForeignKeyTargetSerializer(instance, data=data)
- # self.assertTrue(serializer.is_valid())
- # self.assertEqual(serializer.data, data)
- # serializer.save()
-
- # # Ensure target 1 is updated, and everything else is as expected
- # queryset = ForeignKeyTarget.objects.all()
- # serializer = ForeignKeyTargetSerializer(queryset, many=True)
- # expected = [
- # {'id': 1, 'name': 'target-1', 'sources': [1]},
- # {'id': 2, 'name': 'target-2', 'sources': []},
- # ]
- # self.assertEqual(serializer.data, expected)
-
class PKNullableOneToOneTests(TestCase):
def setUp(self):
@@ -427,116 +448,3 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
self.assertEqual(serializer.data, expected)
-
-
-# The below models and tests ensure that serializer fields corresponding
-# to a ManyToManyField field with a user-specified ``through`` model are
-# set to read only
-
-
-class ManyToManyThroughTarget(models.Model):
- name = models.CharField(max_length=100)
-
-
-class ManyToManyThrough(models.Model):
- source = models.ForeignKey('ManyToManyThroughSource')
- target = models.ForeignKey(ManyToManyThroughTarget)
-
-
-class ManyToManyThroughSource(models.Model):
- name = models.CharField(max_length=100)
- targets = models.ManyToManyField(ManyToManyThroughTarget,
- related_name='sources',
- through='ManyToManyThrough')
-
-
-class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughTarget
- fields = ('id', 'name', 'sources')
-
-
-class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
- class Meta:
- model = ManyToManyThroughSource
- fields = ('id', 'name', 'targets')
-
-
-class PKManyToManyThroughTests(TestCase):
- def setUp(self):
- self.source = ManyToManyThroughSource.objects.create(
- name='through-source-1')
- self.target = ManyToManyThroughTarget.objects.create(
- name='through-target-1')
-
- def test_many_to_many_create(self):
- data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
- serializer = ManyToManyThroughSourceSerializer(data=data)
- self.assertTrue(serializer.fields['targets'].read_only)
- self.assertTrue(serializer.is_valid())
- obj = serializer.save()
- self.assertEqual(obj.name, 'source-2')
- self.assertEqual(obj.targets.count(), 0)
-
- def test_many_to_many_reverse_create(self):
- data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
- serializer = ManyToManyThroughTargetSerializer(data=data)
- self.assertTrue(serializer.fields['sources'].read_only)
- self.assertTrue(serializer.is_valid())
- serializer.save()
- obj = serializer.save()
- self.assertEqual(obj.name, 'target-2')
- self.assertEqual(obj.sources.count(), 0)
-
-
-# Regression tests for #694 (`source` attribute on related fields)
-
-
-class PrimaryKeyRelatedFieldSourceTests(TestCase):
- def test_related_manager_source(self):
- """
- Relational fields should be able to use manager-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
-
- class ClassWithManagerMethod(object):
- def get_blogposts_manager(self):
- return BlogPost.objects
-
- obj = ClassWithManagerMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_related_queryset_source(self):
- """
- Relational fields should be able to use queryset-returning methods as their source.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
-
- class ClassWithQuerysetMethod(object):
- def get_blogposts_queryset(self):
- return BlogPost.objects.all()
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
-
- def test_dotted_source(self):
- """
- Source argument should support dotted.source notation.
- """
- BlogPost.objects.create(title='blah')
- field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
-
- class ClassWithQuerysetMethod(object):
- a = {
- 'b': {
- 'c': BlogPost.objects.all()
- }
- }
-
- obj = ClassWithQuerysetMethod()
- value = field.field_to_native(obj, 'field_name')
- self.assertEqual(value, [1])
diff --git a/rest_framework/tests/test_relations_slug.py b/tests/test_relations_slug.py
index 435c821c..cd2cb1ed 100644
--- a/rest_framework/tests/test_relations_slug.py
+++ b/tests/test_relations_slug.py
@@ -1,24 +1,35 @@
from django.test import TestCase
from rest_framework import serializers
-from rest_framework.tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
+from tests.models import NullableForeignKeySource, ForeignKeySource, ForeignKeyTarget
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
- sources = serializers.SlugRelatedField(many=True, slug_field='name')
+ sources = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeySource.objects.all(),
+ many=True
+ )
class Meta:
model = ForeignKeyTarget
class ForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name')
+ target = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeyTarget.objects.all()
+ )
class Meta:
model = ForeignKeySource
class NullableForeignKeySourceSerializer(serializers.ModelSerializer):
- target = serializers.SlugRelatedField(slug_field='name', required=False)
+ target = serializers.SlugRelatedField(
+ slug_field='name',
+ queryset=ForeignKeyTarget.objects.all(),
+ allow_null=True
+ )
class Meta:
model = NullableForeignKeySource
@@ -43,7 +54,14 @@ class SlugForeignKeyTests(TestCase):
{'id': 2, 'name': 'source-2', 'target': 'target-1'},
{'id': 3, 'name': 'source-3', 'target': 'target-1'}
]
- self.assertEqual(serializer.data, expected)
+ with self.assertNumQueries(4):
+ self.assertEqual(serializer.data, expected)
+
+ def test_foreign_key_retrieve_select_related(self):
+ queryset = ForeignKeySource.objects.all().select_related('target')
+ serializer = ForeignKeySourceSerializer(queryset, many=True)
+ with self.assertNumQueries(1):
+ serializer.data
def test_reverse_foreign_key_retrieve(self):
queryset = ForeignKeyTarget.objects.all()
@@ -54,13 +72,19 @@ class SlugForeignKeyTests(TestCase):
]
self.assertEqual(serializer.data, expected)
+ def test_reverse_foreign_key_retrieve_prefetch_related(self):
+ queryset = ForeignKeyTarget.objects.all().prefetch_related('sources')
+ serializer = ForeignKeyTargetSerializer(queryset, many=True)
+ with self.assertNumQueries(2):
+ serializer.data
+
def test_foreign_key_update(self):
data = {'id': 1, 'name': 'source-1', 'target': 'target-2'}
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = ForeignKeySource.objects.all()
@@ -149,7 +173,7 @@ class SlugForeignKeyTests(TestCase):
instance = ForeignKeySource.objects.get(pk=1)
serializer = ForeignKeySourceSerializer(instance, data=data)
self.assertFalse(serializer.is_valid())
- self.assertEqual(serializer.errors, {'target': ['This field is required.']})
+ self.assertEqual(serializer.errors, {'target': ['This field may not be null.']})
class SlugNullableForeignKeyTests(TestCase):
@@ -220,8 +244,8 @@ class SlugNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, data)
serializer.save()
+ self.assertEqual(serializer.data, data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
@@ -243,8 +267,8 @@ class SlugNullableForeignKeyTests(TestCase):
instance = NullableForeignKeySource.objects.get(pk=1)
serializer = NullableForeignKeySourceSerializer(instance, data=data)
self.assertTrue(serializer.is_valid())
- self.assertEqual(serializer.data, expected_data)
serializer.save()
+ self.assertEqual(serializer.data, expected_data)
# Ensure source 1 is updated, and everything else is as expected
queryset = NullableForeignKeySource.objects.all()
diff --git a/rest_framework/tests/test_renderers.py b/tests/test_renderers.py
index df6f4aa6..cb76f683 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -1,37 +1,47 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-
-from decimal import Decimal
+from django.conf.urls import patterns, url, include
from django.core.cache import cache
+from django.db import models
from django.test import TestCase
-from django.utils import unittest
+from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
-from rest_framework.compat import yaml, etree, patterns, url, include, six, StringIO
+from rest_framework.compat import OrderedDict
from rest_framework.response import Response
from rest_framework.views import APIView
-from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
-from rest_framework.parsers import YAMLParser, XMLParser
+from rest_framework import serializers
+from rest_framework.renderers import (
+ BaseRenderer, JSONRenderer, BrowsableAPIRenderer, HTMLFormRenderer
+)
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
-import datetime
-import pickle
+from collections import MutableMapping
+import json
import re
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+ return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+ return ('Renderer B: %s' % x).encode('ascii')
expected_results = [
- ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator
+ ((elem for elem in [1, 2, 3]), JSONRenderer, b'[1,2,3]') # Generator
]
+class DummyTestModel(models.Model):
+ name = models.CharField(max_length=42, default='')
+
+
class BasicRendererTests(TestCase):
def test_expected_results(self):
for value, renderer_cls, expected in expected_results:
@@ -64,11 +74,22 @@ class MockView(APIView):
class MockGETView(APIView):
-
def get(self, request, **kwargs):
return Response({'foo': ['bar', 'baz']})
+class MockPOSTView(APIView):
+ def post(self, request, **kwargs):
+ return Response({'foo': request.DATA})
+
+
+class EmptyGETView(APIView):
+ renderer_classes = (JSONRenderer,)
+
+ def get(self, request, **kwargs):
+ return Response(status=status.HTTP_204_NO_CONTENT)
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -82,14 +103,15 @@ class HTMLView1(APIView):
def get(self, request, **kwargs):
return Response('text')
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()),
- url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
- url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
+ url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
+ url(r'^empty$', EmptyGETView.as_view()),
url(r'^api', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -131,7 +153,7 @@ class RendererEndToEndTests(TestCase):
End-to-end testing of renderers using an RendererMixin on a generic view.
"""
- urls = 'rest_framework.tests.test_renderers'
+ urls = 'tests.test_renderers'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
@@ -219,8 +241,36 @@ class RendererEndToEndTests(TestCase):
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
+ def test_parse_error_renderers_browsable_api(self):
+ """Invalid data should still render the browsable API correctly."""
+ resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
+ self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
-_flat_repr = '{"foo": ["bar", "baz"]}'
+ def test_204_no_content_responses_have_no_content_type_set(self):
+ """
+ Regression test for #1196
+
+ https://github.com/tomchristie/django-rest-framework/issues/1196
+ """
+ resp = self.client.get('/empty')
+ self.assertEqual(resp.get('Content-Type', None), None)
+ self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT)
+
+ def test_contains_headers_of_api_response(self):
+ """
+ Issue #1437
+
+ Test we display the headers of the API response and not those from the
+ HTML response
+ """
+ resp = self.client.get('/html1')
+ self.assertContains(resp, '>GET, HEAD, OPTIONS<')
+ self.assertContains(resp, '>application/json<')
+ self.assertNotContains(resp, '>text/html; charset=utf-8<')
+
+
+_flat_repr = '{"foo":["bar","baz"]}'
_indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}'
@@ -244,6 +294,66 @@ class JSONRendererTests(TestCase):
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
+ def test_render_queryset_values(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [{'id': o.id, 'name': o.name}])
+
+ def test_render_queryset_values_list(self):
+ o = DummyTestModel.objects.create(name='dummy')
+ qs = DummyTestModel.objects.values_list('id', 'name')
+ ret = JSONRenderer().render(qs)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, [[o.id, o.name]])
+
+ def test_render_dict_abc_obj(self):
+ class Dict(MutableMapping):
+ def __init__(self):
+ self._dict = dict()
+
+ def __getitem__(self, key):
+ return self._dict.__getitem__(key)
+
+ def __setitem__(self, key, value):
+ return self._dict.__setitem__(key, value)
+
+ def __delitem__(self, key):
+ return self._dict.__delitem__(key)
+
+ def __iter__(self):
+ return self._dict.__iter__()
+
+ def __len__(self):
+ return self._dict.__len__()
+
+ def keys(self):
+ return self._dict.keys()
+
+ x = Dict()
+ x['key'] = 'string value'
+ x[2] = 3
+ ret = JSONRenderer().render(x)
+ data = json.loads(ret.decode('utf-8'))
+ self.assertEquals(data, {'key': 'string value', '2': 3})
+
+ def test_render_obj_with_getitem(self):
+ class DictLike(object):
+ def __init__(self):
+ self._dict = {}
+
+ def set(self, value):
+ self._dict = dict(value)
+
+ def __getitem__(self, key):
+ return self._dict[key]
+
+ x = DictLike()
+ x.set({'a': 1, 'b': 'string'})
+ with self.assertRaises(TypeError):
+ JSONRenderer().render(x)
+
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
@@ -263,12 +373,6 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
- def test_check_ascii(self):
- obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = JSONRenderer()
- content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8'))
-
class UnicodeJSONRendererTests(TestCase):
"""
@@ -276,181 +380,31 @@ class UnicodeJSONRendererTests(TestCase):
"""
def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
- renderer = UnicodeJSONRenderer()
+ renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
- self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8'))
-
-
-class JSONPRendererTests(TestCase):
- """
- Tests specific to the JSONP Renderer
- """
-
- urls = 'rest_framework.tests.test_renderers'
-
- def test_without_callback_with_json_renderer(self):
- """
- Test JSONP rendering with View JSON Renderer.
- """
- resp = self.client.get('/jsonp/jsonrenderer',
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
-
- def test_without_callback_without_json_renderer(self):
- """
- Test JSONP rendering without View JSON Renderer.
- """
- resp = self.client.get('/jsonp/nojsonrenderer',
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
-
- def test_with_callback(self):
- """
- Test JSONP rendering with callback function name.
- """
- callback_func = 'myjsonpcallback'
- resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
- HTTP_ACCEPT='application/javascript')
- self.assertEqual(resp.status_code, status.HTTP_200_OK)
- self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
-
-
-if yaml:
- _yaml_repr = 'foo: [bar, baz]\n'
+ self.assertEqual(content, '{"countries":["United Kingdom","France","España"]}'.encode('utf-8'))
- class YAMLRendererTests(TestCase):
- """
- Tests specific to the JSON Renderer
- """
-
- def test_render(self):
- """
- Test basic YAML rendering.
- """
- obj = {'foo': ['bar', 'baz']}
- renderer = YAMLRenderer()
- content = renderer.render(obj, 'application/yaml')
- self.assertEqual(content, _yaml_repr)
-
- def test_render_and_parse(self):
- """
- Test rendering and then parsing returns the original object.
- IE obj -> render -> parse -> obj.
- """
- obj = {'foo': ['bar', 'baz']}
-
- renderer = YAMLRenderer()
- parser = YAMLParser()
-
- content = renderer.render(obj, 'application/yaml')
- data = parser.parse(StringIO(content))
- self.assertEqual(obj, data)
+ def test_u2028_u2029(self):
+ # The \u2028 and \u2029 characters should be escaped,
+ # even when the non-escaping unicode representation is used.
+ # Regression test for #2169
+ obj = {'should_escape': '\u2028\u2029'}
+ renderer = JSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"should_escape":"\\u2028\\u2029"}'.encode('utf-8'))
-class XMLRendererTestCase(TestCase):
+class AsciiJSONRendererTests(TestCase):
"""
- Tests specific to the XML Renderer
+ Tests specific for the Unicode JSON Renderer
"""
-
- _complex_data = {
- "creation_date": datetime.datetime(2011, 12, 25, 12, 45, 00),
- "name": "name",
- "sub_data_list": [
- {
- "sub_id": 1,
- "sub_name": "first"
- },
- {
- "sub_id": 2,
- "sub_name": "second"
- }
- ]
- }
-
- def test_render_string(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 'astring'}, 'application/xml')
- self.assertXMLContains(content, '<field>astring</field>')
-
- def test_render_integer(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 111}, 'application/xml')
- self.assertXMLContains(content, '<field>111</field>')
-
- def test_render_datetime(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({
- 'field': datetime.datetime(2011, 12, 25, 12, 45, 00)
- }, 'application/xml')
- self.assertXMLContains(content, '<field>2011-12-25 12:45:00</field>')
-
- def test_render_float(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': 123.4}, 'application/xml')
- self.assertXMLContains(content, '<field>123.4</field>')
-
- def test_render_decimal(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': Decimal('111.2')}, 'application/xml')
- self.assertXMLContains(content, '<field>111.2</field>')
-
- def test_render_none(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render({'field': None}, 'application/xml')
- self.assertXMLContains(content, '<field></field>')
-
- def test_render_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = renderer.render(self._complex_data, 'application/xml')
- self.assertXMLContains(content, '<sub_name>first</sub_name>')
- self.assertXMLContains(content, '<sub_name>second</sub_name>')
-
- @unittest.skipUnless(etree, 'defusedxml not installed')
- def test_render_and_parse_complex_data(self):
- """
- Test XML rendering.
- """
- renderer = XMLRenderer()
- content = StringIO(renderer.render(self._complex_data, 'application/xml'))
-
- parser = XMLParser()
- complex_data_out = parser.parse(content)
- error_msg = "complex data differs!IN:\n %s \n\n OUT:\n %s" % (repr(self._complex_data), repr(complex_data_out))
- self.assertEqual(self._complex_data, complex_data_out, error_msg)
-
- def assertXMLContains(self, xml, string):
- self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
- self.assertTrue(xml.endswith('</root>'))
- self.assertTrue(string in xml, '%r not in %r' % (string, xml))
+ def test_proper_encoding(self):
+ class AsciiJSONRenderer(JSONRenderer):
+ ensure_ascii = True
+ obj = {'countries': ['United Kingdom', 'France', 'España']}
+ renderer = AsciiJSONRenderer()
+ content = renderer.render(obj, 'application/json')
+ self.assertEqual(content, '{"countries":["United Kingdom","France","Espa\\u00f1a"]}'.encode('utf-8'))
# Tests for caching issue, #346
@@ -459,81 +413,61 @@ class CacheRenderTest(TestCase):
Tests specific to caching responses
"""
- urls = 'rest_framework.tests.test_renderers'
-
- cache_key = 'just_a_cache_key'
-
- @classmethod
- def _get_pickling_errors(cls, obj, seen=None):
- """ Return any errors that would be raised if `obj' is pickled
- Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
- """
- if seen == None:
- seen = []
- try:
- state = obj.__getstate__()
- except AttributeError:
- return
- if state == None:
- return
- if isinstance(state, tuple):
- if not isinstance(state[0], dict):
- state = state[1]
- else:
- state = state[0].update(state[1])
- result = {}
- for i in state:
- try:
- pickle.dumps(state[i], protocol=2)
- except pickle.PicklingError:
- if not state[i] in seen:
- seen.append(state[i])
- result[i] = cls._get_pickling_errors(state[i], seen)
- return result
-
- def http_resp(self, http_method, url):
- """
- Simple wrapper for Client http requests
- Removes the `client' and `request' attributes from as they are
- added by django.test.client.Client and not part of caching
- responses outside of tests.
- """
- method = getattr(self.client, http_method)
- resp = method(url)
- del resp.client, resp.request
- return resp
-
- def test_obj_pickling(self):
- """
- Test that responses are properly pickled
- """
- resp = self.http_resp('get', '/cache')
-
- # Make sure that no pickling errors occurred
- self.assertEqual(self._get_pickling_errors(resp), {})
-
- # Unfortunately LocMem backend doesn't raise PickleErrors but returns
- # None instead.
- cache.set(self.cache_key, resp)
- self.assertTrue(cache.get(self.cache_key) is not None)
+ urls = 'tests.test_renderers'
def test_head_caching(self):
"""
Test caching of HEAD requests
"""
- resp = self.http_resp('head', '/cache')
- cache.set(self.cache_key, resp)
-
- cached_resp = cache.get(self.cache_key)
- self.assertIsInstance(cached_resp, Response)
+ response = self.client.head('/cache')
+ cache.set('key', response)
+ cached_response = cache.get('key')
+ assert isinstance(cached_response, Response)
+ assert cached_response.content == response.content
+ assert cached_response.status_code == response.status_code
def test_get_caching(self):
"""
Test caching of GET requests
"""
- resp = self.http_resp('get', '/cache')
- cache.set(self.cache_key, resp)
+ response = self.client.get('/cache')
+ cache.set('key', response)
+ cached_response = cache.get('key')
+ assert isinstance(cached_response, Response)
+ assert cached_response.content == response.content
+ assert cached_response.status_code == response.status_code
- cached_resp = cache.get(self.cache_key)
- self.assertIsInstance(cached_resp, Response)
- self.assertEqual(cached_resp.content, resp.content)
+
+class TestJSONIndentationStyles:
+ def test_indented(self):
+ renderer = JSONRenderer()
+ data = OrderedDict([('a', 1), ('b', 2)])
+ assert renderer.render(data) == b'{"a":1,"b":2}'
+
+ def test_compact(self):
+ renderer = JSONRenderer()
+ data = OrderedDict([('a', 1), ('b', 2)])
+ context = {'indent': 4}
+ assert (
+ renderer.render(data, renderer_context=context) ==
+ b'{\n "a": 1,\n "b": 2\n}'
+ )
+
+ def test_long_form(self):
+ renderer = JSONRenderer()
+ renderer.compact = False
+ data = OrderedDict([('a', 1), ('b', 2)])
+ assert renderer.render(data) == b'{"a": 1, "b": 2}'
+
+
+class TestHiddenFieldHTMLFormRenderer(TestCase):
+ def test_hidden_field_rendering(self):
+ class TestSerializer(serializers.Serializer):
+ published = serializers.HiddenField(default=True)
+
+ serializer = TestSerializer(data={})
+ serializer.is_valid()
+ renderer = HTMLFormRenderer()
+ field = serializer['published']
+ rendered = renderer.render_field(field, {})
+ assert rendered == ''
diff --git a/rest_framework/tests/test_request.py b/tests/test_request.py
index 969d8024..c274ab69 100644
--- a/rest_framework/tests/test_request.py
+++ b/tests/test_request.py
@@ -2,25 +2,27 @@
Tests for content parsing, and form-overloaded content parsing.
"""
from __future__ import unicode_literals
+from django.conf.urls import patterns
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout
from django.contrib.sessions.middleware import SessionMiddleware
+from django.core.handlers.wsgi import WSGIRequest
from django.test import TestCase
+from django.utils import six
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
-from rest_framework.compat import patterns
from rest_framework.parsers import (
BaseParser,
FormParser,
MultiPartParser,
JSONParser
)
-from rest_framework.request import Request
+from rest_framework.request import Request, Empty
from rest_framework.response import Response
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory, APIClient
from rest_framework.views import APIView
-from rest_framework.compat import six
+from io import BytesIO
import json
@@ -66,6 +68,9 @@ class TestMethodOverloading(TestCase):
request = Request(factory.post('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
self.assertEqual(request.method, 'DELETE')
+ request = Request(factory.get('/', {'foo': 'bar'}, HTTP_X_HTTP_METHOD_OVERRIDE='DELETE'))
+ self.assertEqual(request.method, 'DELETE')
+
class TestContentParsing(TestCase):
def test_standard_behaviour_determines_no_content_GET(self):
@@ -146,88 +151,33 @@ class TestContentParsing(TestCase):
request.parsers = (JSONParser(), )
self.assertEqual(request.DATA, json_data)
- # def test_accessing_post_after_data_form(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in
- # form request.
- # """
- # data = {'qwerty': 'uiop'}
- # request = factory.post('/', data=data)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), data.items())
-
- # def test_accessing_post_after_data_for_json(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
-
- # request = factory.post('/', content, content_type=content_type,
- # parsers=parsers)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), [])
-
- # def test_accessing_post_after_data_for_overloaded_json(self):
- # """
- # Ensures request.POST can be accessed after request.DATA in overloaded
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # form_data = {Request._CONTENT_PARAM: content,
- # Request._CONTENTTYPE_PARAM: content_type}
-
- # request = factory.post('/', form_data, parsers=parsers)
- # self.assertEqual(request.DATA.items(), data.items())
- # self.assertEqual(request.POST.items(), form_data.items())
-
- # def test_accessing_data_after_post_form(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in
- # form request.
- # """
- # data = {'qwerty': 'uiop'}
- # parsers = (FormParser, MultiPartParser)
- # request = factory.post('/', data, parsers=parsers)
-
- # self.assertEqual(request.POST.items(), data.items())
- # self.assertEqual(request.DATA.items(), data.items())
-
- # def test_accessing_data_after_post_for_json(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in
- # json request.
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # request = factory.post('/', content, content_type=content_type,
- # parsers=parsers)
- # self.assertEqual(request.POST.items(), [])
- # self.assertEqual(request.DATA.items(), data.items())
-
- # def test_accessing_data_after_post_for_overloaded_json(self):
- # """
- # Ensures request.DATA can be accessed after request.POST in overloaded
- # json request
- # """
- # data = {'qwerty': 'uiop'}
- # content = json.dumps(data)
- # content_type = 'application/json'
- # parsers = (JSONParser, )
- # form_data = {Request._CONTENT_PARAM: content,
- # Request._CONTENTTYPE_PARAM: content_type}
-
- # request = factory.post('/', form_data, parsers=parsers)
- # self.assertEqual(request.POST.items(), form_data.items())
- # self.assertEqual(request.DATA.items(), data.items())
+ def test_form_POST_unicode(self):
+ """
+ JSON POST via default web interface with unicode data
+ """
+ # Note: environ and other variables here have simplified content compared to real Request
+ CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D'
+ environ = {
+ 'REQUEST_METHOD': 'POST',
+ 'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+ 'CONTENT_LENGTH': len(CONTENT),
+ 'wsgi.input': BytesIO(CONTENT),
+ }
+ wsgi_request = WSGIRequest(environ=environ)
+ wsgi_request._load_post_and_files()
+ parsers = (JSONParser(), FormParser(), MultiPartParser())
+ parser_context = {
+ 'encoding': 'utf-8',
+ 'kwargs': {},
+ 'args': (),
+ }
+ request = Request(wsgi_request, parsers=parsers, parser_context=parser_context)
+ method = request.method
+ self.assertEqual(method, 'POST')
+ self.assertEqual(request._content_type, 'application/json')
+ self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}')
+ self.assertEqual(request._data, Empty)
+ self.assertEqual(request._files, Empty)
class MockView(APIView):
@@ -237,15 +187,16 @@ class MockView(APIView):
if request.POST.get('example') is not None:
return Response(status=status.HTTP_200_OK)
- return Response(status=status.INTERNAL_SERVER_ERROR)
+ return Response(status=status.HTTP_500_INTERNAL_SERVER_ERROR)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
(r'^$', MockView.as_view()),
)
class TestContentParsingWithAuthentication(TestCase):
- urls = 'rest_framework.tests.test_request'
+ urls = 'tests.test_request'
def setUp(self):
self.csrf_client = APIClient(enforce_csrf_checks=True)
@@ -267,25 +218,14 @@ class TestContentParsingWithAuthentication(TestCase):
response = self.csrf_client.post('/', content)
self.assertEqual(status.HTTP_200_OK, response.status_code)
- # def test_user_logged_in_authentication_has_post_when_logged_in(self):
- # """Ensures request.POST exists after UserLoggedInAuthentication when user does log in"""
- # self.client.login(username='john', password='password')
- # self.csrf_client.login(username='john', password='password')
- # content = {'example': 'example'}
-
- # response = self.client.post('/', content)
- # self.assertEqual(status.OK, response.status_code, "POST data is malformed")
-
- # response = self.csrf_client.post('/', content)
- # self.assertEqual(status.OK, response.status_code, "POST data is malformed")
-
class TestUserSetter(TestCase):
def setUp(self):
# Pass request object through session middleware so session is
# available to login and logout functions
- self.request = Request(factory.get('/'))
+ self.wrapped_request = factory.get('/')
+ self.request = Request(self.wrapped_request)
SessionMiddleware().process_request(self.request)
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
@@ -305,9 +245,33 @@ class TestUserSetter(TestCase):
logout(self.request)
self.assertTrue(self.request.user.is_anonymous())
+ def test_logged_in_user_is_set_on_wrapped_request(self):
+ login(self.request, self.user)
+ self.assertEqual(self.wrapped_request.user, self.user)
-class TestAuthSetter(TestCase):
+ def test_calling_user_fails_when_attribute_error_is_raised(self):
+ """
+ This proves that when an AttributeError is raised inside of the request.user
+ property, that we can handle this and report the true, underlying error.
+ """
+ class AuthRaisesAttributeError(object):
+ def authenticate(self, request):
+ import rest_framework
+ rest_framework.MISSPELLED_NAME_THAT_DOESNT_EXIST
+
+ self.request = Request(factory.get('/'), authenticators=(AuthRaisesAttributeError(),))
+ SessionMiddleware().process_request(self.request)
+ login(self.request, self.user)
+ try:
+ self.request.user
+ except AttributeError as error:
+ self.assertEqual(str(error), "'module' object has no attribute 'MISSPELLED_NAME_THAT_DOESNT_EXIST'")
+ else:
+ assert False, 'AttributeError not raised'
+
+
+class TestAuthSetter(TestCase):
def test_auth_can_be_set(self):
request = Request(factory.get('/'))
request.auth = 'DUMMY'
diff --git a/rest_framework/tests/test_response.py b/tests/test_response.py
index eea3c641..4a9deaa2 100644
--- a/rest_framework/tests/test_response.py
+++ b/tests/test_response.py
@@ -1,11 +1,13 @@
from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
from django.test import TestCase
-from rest_framework.tests.models import BasicModel, BasicModelSerializer
-from rest_framework.compat import patterns, url, include
+from django.utils import six
+from tests.models import BasicModel
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework import generics
from rest_framework import routers
+from rest_framework import serializers
from rest_framework import status
from rest_framework.renderers import (
BaseRenderer,
@@ -14,7 +16,12 @@ from rest_framework.renderers import (
)
from rest_framework import viewsets
from rest_framework.settings import api_settings
-from rest_framework.compat import six
+
+
+# Serializer used to test BasicModel
+class BasicModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = BasicModel
class MockPickleRenderer(BaseRenderer):
@@ -31,8 +38,13 @@ class MockTextMediaRenderer(BaseRenderer):
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
-RENDERER_A_SERIALIZER = lambda x: ('Renderer A: %s' % x).encode('ascii')
-RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
+
+def RENDERER_A_SERIALIZER(x):
+ return ('Renderer A: %s' % x).encode('ascii')
+
+
+def RENDERER_B_SERIALIZER(x):
+ return ('Renderer B: %s' % x).encode('ascii')
class RendererA(BaseRenderer):
@@ -86,21 +98,23 @@ class HTMLView1(APIView):
class HTMLNewModelViewSet(viewsets.ModelViewSet):
- model = BasicModel
+ serializer_class = BasicModelSerializer
+ queryset = BasicModel.objects.all()
class HTMLNewModelView(generics.ListCreateAPIView):
renderer_classes = (BrowsableAPIRenderer,)
permission_classes = []
serializer_class = BasicModelSerializer
- model = BasicModel
+ queryset = BasicModel.objects.all()
new_model_viewset_router = routers.DefaultRouter()
new_model_viewset_router.register(r'', HTMLNewModelViewSet)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
@@ -118,7 +132,7 @@ class RendererIntegrationTests(TestCase):
End-to-end testing of renderers using an ResponseMixin on a generic view.
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
@@ -198,7 +212,7 @@ class Issue122Tests(TestCase):
"""
Tests that covers #122.
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_only_html_renderer(self):
"""
@@ -218,13 +232,13 @@ class Issue467Tests(TestCase):
Tests for #467
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
class Issue807Tests(TestCase):
@@ -232,7 +246,7 @@ class Issue807Tests(TestCase):
Covers #807
"""
- urls = 'rest_framework.tests.test_response'
+ urls = 'tests.test_response'
def test_does_not_append_charset_by_default(self):
"""
@@ -253,9 +267,9 @@ class Issue807Tests(TestCase):
expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
self.assertEqual(expected, resp['Content-Type'])
- def test_content_type_set_explictly_on_response(self):
+ def test_content_type_set_explicitly_on_response(self):
"""
- The content type may be set explictly on the response.
+ The content type may be set explicitly on the response.
"""
headers = {"HTTP_ACCEPT": RendererC.media_type}
resp = self.client.get('/setbyview', **headers)
@@ -268,11 +282,11 @@ class Issue807Tests(TestCase):
)
resp = self.client.get('/html_new_model_viewset/' + param)
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
- self.assertContains(resp, 'Text comes here')
- self.assertContains(resp, 'Text description.')
+ # self.assertContains(resp, 'Text comes here')
+ # self.assertContains(resp, 'Text description.')
diff --git a/rest_framework/tests/test_reverse.py b/tests/test_reverse.py
index 690a30b1..675a9d5a 100644
--- a/rest_framework/tests/test_reverse.py
+++ b/tests/test_reverse.py
@@ -1,6 +1,6 @@
from __future__ import unicode_literals
+from django.conf.urls import patterns, url
from django.test import TestCase
-from rest_framework.compat import patterns, url
from rest_framework.reverse import reverse
from rest_framework.test import APIRequestFactory
@@ -10,7 +10,8 @@ factory = APIRequestFactory()
def null_view(request):
pass
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^view$', null_view, name='view'),
)
@@ -19,7 +20,7 @@ class ReverseTests(TestCase):
"""
Tests for fully qualified URLs when using `reverse`.
"""
- urls = 'rest_framework.tests.test_reverse'
+ urls = 'tests.test_reverse'
def test_reversed_urls_are_fully_qualified(self):
request = factory.get('/view')
diff --git a/tests/test_routers.py b/tests/test_routers.py
new file mode 100644
index 00000000..08c58ec7
--- /dev/null
+++ b/tests/test_routers.py
@@ -0,0 +1,348 @@
+from __future__ import unicode_literals
+from django.conf.urls import url, include
+from django.db import models
+from django.test import TestCase
+from django.core.exceptions import ImproperlyConfigured
+from rest_framework import serializers, viewsets, permissions
+from rest_framework.decorators import detail_route, list_route
+from rest_framework.response import Response
+from rest_framework.routers import SimpleRouter, DefaultRouter
+from rest_framework.test import APIRequestFactory
+from collections import namedtuple
+
+factory = APIRequestFactory()
+
+
+class RouterTestModel(models.Model):
+ uuid = models.CharField(max_length=20)
+ text = models.CharField(max_length=200)
+
+
+class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
+
+ class Meta:
+ model = RouterTestModel
+ fields = ('url', 'uuid', 'text')
+
+
+class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ serializer_class = NoteSerializer
+ lookup_field = 'uuid'
+
+
+class MockViewSet(viewsets.ModelViewSet):
+ queryset = None
+ serializer_class = None
+
+
+notes_router = SimpleRouter()
+notes_router.register(r'notes', NoteViewSet)
+
+namespaced_router = DefaultRouter()
+namespaced_router.register(r'example', MockViewSet, base_name='example')
+
+urlpatterns = [
+ url(r'^non-namespaced/', include(namespaced_router.urls)),
+ url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
+ url(r'^example/', include(notes_router.urls)),
+]
+
+
+class BasicViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'method': 'list'})
+
+ @detail_route(methods=['post'])
+ def action1(self, request, *args, **kwargs):
+ return Response({'method': 'action1'})
+
+ @detail_route(methods=['post'])
+ def action2(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @detail_route(methods=['post', 'delete'])
+ def action3(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @detail_route()
+ def link1(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route()
+ def link2(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+
+class TestSimpleRouter(TestCase):
+ def setUp(self):
+ self.router = SimpleRouter()
+
+ def test_link_and_action_decorator(self):
+ routes = self.router.get_routes(BasicViewSet)
+ decorator_routes = routes[2:]
+ # Make sure all these endpoints exist and none have been clobbered
+ for i, endpoint in enumerate(['action1', 'action2', 'action3', 'link1', 'link2']):
+ route = decorator_routes[i]
+ # check url listing
+ self.assertEqual(route.url,
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ # check method to function mapping
+ if endpoint == 'action3':
+ methods_map = ['post', 'delete']
+ elif endpoint.startswith('action'):
+ methods_map = ['post']
+ else:
+ methods_map = ['get']
+ for method in methods_map:
+ self.assertEqual(route.mapping[method], endpoint)
+
+
+class TestRootView(TestCase):
+ urls = 'tests.test_routers'
+
+ def test_retrieve_namespaced_root(self):
+ response = self.client.get('/namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/namespaced/example/",
+ }
+ )
+
+ def test_retrieve_non_namespaced_root(self):
+ response = self.client.get('/non-namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/non-namespaced/example/",
+ }
+ )
+
+
+class TestCustomLookupFields(TestCase):
+ """
+ Ensure that custom lookup fields are correctly routed.
+ """
+ urls = 'tests.test_routers'
+
+ def setUp(self):
+ RouterTestModel.objects.create(uuid='123', text='foo bar')
+
+ def test_custom_lookup_field_route(self):
+ detail_route = notes_router.urls[-1]
+ detail_url_pattern = detail_route.regex.pattern
+ self.assertIn('<uuid>', detail_url_pattern)
+
+ def test_retrieve_lookup_field_list_view(self):
+ response = self.client.get('/example/notes/')
+ self.assertEqual(
+ response.data,
+ [{
+ "url": "http://testserver/example/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }]
+ )
+
+ def test_retrieve_lookup_field_detail_view(self):
+ response = self.client.get('/example/notes/123/')
+ self.assertEqual(
+ response.data,
+ {
+ "url": "http://testserver/example/notes/123/",
+ "uuid": "123", "text": "foo bar"
+ }
+ )
+
+
+class TestLookupValueRegex(TestCase):
+ """
+ Ensure the router honors lookup_value_regex when applied
+ to the viewset.
+ """
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ lookup_field = 'uuid'
+ lookup_value_regex = '[0-9a-f]{32}'
+
+ self.router = SimpleRouter()
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_limited_by_lookup_value_regex(self):
+ expected = ['^notes/$', '^notes/(?P<uuid>[0-9a-f]{32})/$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestTrailingSlashIncluded(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = SimpleRouter()
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_have_trailing_slash_by_default(self):
+ expected = ['^notes/$', '^notes/(?P<pk>[^/.]+)/$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestTrailingSlashRemoved(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = SimpleRouter(trailing_slash=False)
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_urls_can_have_trailing_slash_removed(self):
+ expected = ['^notes$', '^notes/(?P<pk>[^/.]+)$']
+ for idx in range(len(expected)):
+ self.assertEqual(expected[idx], self.urls[idx].regex.pattern)
+
+
+class TestNameableRoot(TestCase):
+ def setUp(self):
+ class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+
+ self.router = DefaultRouter()
+ self.router.root_view_name = 'nameable-root'
+ self.router.register(r'notes', NoteViewSet)
+ self.urls = self.router.urls
+
+ def test_router_has_custom_name(self):
+ expected = 'nameable-root'
+ self.assertEqual(expected, self.urls[0].name)
+
+
+class TestActionKeywordArgs(TestCase):
+ """
+ Ensure keyword arguments passed in the `@action` decorator
+ are properly handled. Refs #940.
+ """
+
+ def setUp(self):
+ class TestViewSet(viewsets.ModelViewSet):
+ permission_classes = []
+
+ @detail_route(methods=['post'], permission_classes=[permissions.AllowAny])
+ def custom(self, request, *args, **kwargs):
+ return Response({
+ 'permission_classes': self.permission_classes
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+ self.view = self.router.urls[-1].callback
+
+ def test_action_kwargs(self):
+ request = factory.post('/test/0/custom/')
+ response = self.view(request)
+ self.assertEqual(
+ response.data,
+ {'permission_classes': [permissions.AllowAny]}
+ )
+
+
+class TestActionAppliedToExistingRoute(TestCase):
+ """
+ Ensure `@detail_route` decorator raises an except when applied
+ to an existing route
+ """
+
+ def test_exception_raised_when_action_applied_to_existing_route(self):
+ class TestViewSet(viewsets.ModelViewSet):
+
+ @detail_route(methods=['post'])
+ def retrieve(self, request, *args, **kwargs):
+ return Response({
+ 'hello': 'world'
+ })
+
+ self.router = SimpleRouter()
+ self.router.register(r'test', TestViewSet, base_name='test')
+
+ with self.assertRaises(ImproperlyConfigured):
+ self.router.urls
+
+
+class DynamicListAndDetailViewSet(viewsets.ViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'method': 'list'})
+
+ @list_route(methods=['post'])
+ def list_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action1'})
+
+ @detail_route(methods=['post'])
+ def detail_route_post(self, request, *args, **kwargs):
+ return Response({'method': 'action2'})
+
+ @list_route()
+ def list_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route()
+ def detail_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+ @list_route(url_path="list_custom-route")
+ def list_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route(url_path="detail_custom-route")
+ def detail_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
+
+class SubDynamicListAndDetailViewSet(DynamicListAndDetailViewSet):
+ pass
+
+
+class TestDynamicListAndDetailRouter(TestCase):
+ def setUp(self):
+ self.router = SimpleRouter()
+
+ def _test_list_and_detail_route_decorators(self, viewset):
+ routes = self.router.get_routes(viewset)
+ decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
+
+ MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
+ # Make sure all these endpoints exist and none have been clobbered
+ for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
+ MethodNamesMap('list_route_get', 'list_route_get'),
+ MethodNamesMap('list_route_post', 'list_route_post'),
+ MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
+ MethodNamesMap('detail_route_get', 'detail_route_get'),
+ MethodNamesMap('detail_route_post', 'detail_route_post')
+ ]):
+ route = decorator_routes[i]
+ # check url listing
+ method_name = endpoint.method_name
+ url_path = endpoint.url_path
+
+ if method_name.startswith('list_'):
+ self.assertEqual(route.url,
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
+ else:
+ self.assertEqual(route.url,
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
+ # check method to function mapping
+ if method_name.endswith('_post'):
+ method_map = 'post'
+ else:
+ method_map = 'get'
+ self.assertEqual(route.mapping[method_map], method_name)
+
+ def test_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(DynamicListAndDetailViewSet)
+
+ def test_inherited_list_and_detail_route_decorators(self):
+ self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet)
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
new file mode 100644
index 00000000..b7a0484b
--- /dev/null
+++ b/tests/test_serializer.py
@@ -0,0 +1,297 @@
+# coding: utf-8
+from __future__ import unicode_literals
+from .utils import MockObject
+from rest_framework import serializers
+from rest_framework.compat import unicode_repr
+import pickle
+import pytest
+
+
+# Tests for core functionality.
+# -----------------------------
+
+class TestSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+ self.Serializer = ExampleSerializer
+
+ def test_valid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc', 'integer': 123})
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc', 'integer': 123}
+ assert serializer.errors == {}
+
+ def test_invalid_serializer(self):
+ serializer = self.Serializer(data={'char': 'abc'})
+ assert not serializer.is_valid()
+ assert serializer.validated_data == {}
+ assert serializer.errors == {'integer': ['This field is required.']}
+
+ def test_partial_validation(self):
+ serializer = self.Serializer(data={'char': 'abc'}, partial=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'char': 'abc'}
+ assert serializer.errors == {}
+
+ def test_empty_serializer(self):
+ serializer = self.Serializer()
+ assert serializer.data == {'char': '', 'integer': None}
+
+ def test_missing_attribute_during_serialization(self):
+ class MissingAttributes:
+ pass
+ instance = MissingAttributes()
+ serializer = self.Serializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
+
+
+class TestValidateMethod:
+ def test_non_field_error_validate_method(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+
+ def validate(self, attrs):
+ raise serializers.ValidationError('Non field error')
+
+ serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123})
+ assert not serializer.is_valid()
+ assert serializer.errors == {'non_field_errors': ['Non field error']}
+
+ def test_field_error_validate_method(self):
+ class ExampleSerializer(serializers.Serializer):
+ char = serializers.CharField()
+ integer = serializers.IntegerField()
+
+ def validate(self, attrs):
+ raise serializers.ValidationError({'char': 'Field error'})
+
+ serializer = ExampleSerializer(data={'char': 'abc', 'integer': 123})
+ assert not serializer.is_valid()
+ assert serializer.errors == {'char': ['Field error']}
+
+
+class TestBaseSerializer:
+ def setup(self):
+ class ExampleSerializer(serializers.BaseSerializer):
+ def to_representation(self, obj):
+ return {
+ 'id': obj['id'],
+ 'email': obj['name'] + '@' + obj['domain']
+ }
+
+ def to_internal_value(self, data):
+ name, domain = str(data['email']).split('@')
+ return {
+ 'id': int(data['id']),
+ 'name': name,
+ 'domain': domain,
+ }
+
+ self.Serializer = ExampleSerializer
+
+ def test_serialize_instance(self):
+ instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'}
+ serializer = self.Serializer(instance)
+ assert serializer.data == {'id': 1, 'email': 'tom@example.com'}
+
+ def test_serialize_list(self):
+ instances = [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'},
+ ]
+ serializer = self.Serializer(instances, many=True)
+ assert serializer.data == [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'}
+ ]
+
+ def test_validate_data(self):
+ data = {'id': 1, 'email': 'tom@example.com'}
+ serializer = self.Serializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'id': 1,
+ 'name': 'tom',
+ 'domain': 'example.com'
+ }
+
+ def test_validate_list(self):
+ data = [
+ {'id': 1, 'email': 'tom@example.com'},
+ {'id': 2, 'email': 'ann@example.com'},
+ ]
+ serializer = self.Serializer(data=data, many=True)
+ assert serializer.is_valid()
+ assert serializer.validated_data == [
+ {'id': 1, 'name': 'tom', 'domain': 'example.com'},
+ {'id': 2, 'name': 'ann', 'domain': 'example.com'}
+ ]
+
+
+class TestStarredSource:
+ """
+ Tests for `source='*'` argument, which is used for nested representations.
+
+ For example:
+
+ nested_field = NestedField(source='*')
+ """
+ data = {
+ 'nested1': {'a': 1, 'b': 2},
+ 'nested2': {'c': 3, 'd': 4}
+ }
+
+ def setup(self):
+ class NestedSerializer1(serializers.Serializer):
+ a = serializers.IntegerField()
+ b = serializers.IntegerField()
+
+ class NestedSerializer2(serializers.Serializer):
+ c = serializers.IntegerField()
+ d = serializers.IntegerField()
+
+ class TestSerializer(serializers.Serializer):
+ nested1 = NestedSerializer1(source='*')
+ nested2 = NestedSerializer2(source='*')
+
+ self.Serializer = TestSerializer
+
+ def test_nested_validate(self):
+ """
+ A nested representation is validated into a flat internal object.
+ """
+ serializer = self.Serializer(data=self.data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'a': 1,
+ 'b': 2,
+ 'c': 3,
+ 'd': 4
+ }
+
+ def test_nested_serialize(self):
+ """
+ An object can be serialized into a nested representation.
+ """
+ instance = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
+ serializer = self.Serializer(instance)
+ assert serializer.data == self.data
+
+
+class TestIncorrectlyConfigured:
+ def test_incorrect_field_name(self):
+ class ExampleSerializer(serializers.Serializer):
+ incorrect_name = serializers.IntegerField()
+
+ class ExampleObject:
+ def __init__(self):
+ self.correct_name = 123
+
+ instance = ExampleObject()
+ serializer = ExampleSerializer(instance)
+ with pytest.raises(AttributeError) as exc_info:
+ serializer.data
+ msg = str(exc_info.value)
+ assert msg.startswith(
+ "Got AttributeError when attempting to get a value for field `incorrect_name` on serializer `ExampleSerializer`.\n"
+ "The serializer field might be named incorrectly and not match any attribute or key on the `ExampleObject` instance.\n"
+ "Original exception text was:"
+ )
+
+
+class TestUnicodeRepr:
+ def test_unicode_repr(self):
+ class ExampleSerializer(serializers.Serializer):
+ example = serializers.CharField()
+
+ class ExampleObject:
+ def __init__(self):
+ self.example = '한국'
+
+ def __repr__(self):
+ return unicode_repr(self.example)
+
+ instance = ExampleObject()
+ serializer = ExampleSerializer(instance)
+ repr(serializer) # Should not error.
+
+
+class TestNotRequiredOutput:
+ def test_not_required_output_for_dict(self):
+ """
+ 'required=False' should allow a dictionary key to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_not_required_output_for_object(self):
+ """
+ 'required=False' should allow an object attribute to be missing in output.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(required=False)
+ included = serializers.CharField()
+
+ def create(self, validated_data):
+ return MockObject(**validated_data)
+
+ serializer = ExampleSerializer(data={'included': 'abc'})
+ serializer.is_valid()
+ serializer.save()
+ assert serializer.data == {'included': 'abc'}
+
+ def test_default_required_output_for_dict(self):
+ """
+ 'default="something"' should require dictionary key.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ serializer = ExampleSerializer({'included': 'abc'})
+ with pytest.raises(KeyError):
+ serializer.data
+
+ def test_default_required_output_for_object(self):
+ """
+ 'default="something"' should require object attribute.
+
+ We need to handle this as the field will have an implicit
+ 'required=False', but it should still have a value.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ omitted = serializers.CharField(default='abc')
+ included = serializers.CharField()
+
+ instance = MockObject(included='abc')
+ serializer = ExampleSerializer(instance)
+ with pytest.raises(AttributeError):
+ serializer.data
+
+
+class TestCacheSerializerData:
+ def test_cache_serializer_data(self):
+ """
+ Caching serializer data with pickle will drop the serializer info,
+ but does preserve the data itself.
+ """
+ class ExampleSerializer(serializers.Serializer):
+ field1 = serializers.CharField()
+ field2 = serializers.CharField()
+
+ serializer = ExampleSerializer({'field1': 'a', 'field2': 'b'})
+ pickled = pickle.dumps(serializer.data)
+ data = pickle.loads(pickled)
+ assert data == {'field1': 'a', 'field2': 'b'}
diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py
new file mode 100644
index 00000000..bc955b2e
--- /dev/null
+++ b/tests/test_serializer_bulk_update.py
@@ -0,0 +1,123 @@
+"""
+Tests to cover bulk create and update using serializers.
+"""
+from __future__ import unicode_literals
+from django.test import TestCase
+from django.utils import six
+from rest_framework import serializers
+
+
+class BulkCreateSerializerTests(TestCase):
+ """
+ Creating multiple instances using serializers.
+ """
+
+ def setUp(self):
+ class BookSerializer(serializers.Serializer):
+ id = serializers.IntegerField()
+ title = serializers.CharField(max_length=100)
+ author = serializers.CharField(max_length=100)
+
+ self.BookSerializer = BookSerializer
+
+ def test_bulk_create_success(self):
+ """
+ Correct bulk update serialization should return the input data.
+ """
+
+ data = [
+ {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }, {
+ 'id': 1,
+ 'title': 'If this is a man',
+ 'author': 'Primo Levi'
+ }, {
+ 'id': 2,
+ 'title': 'The wind-up bird chronicle',
+ 'author': 'Haruki Murakami'
+ }
+ ]
+
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), True)
+ self.assertEqual(serializer.validated_data, data)
+
+ def test_bulk_create_errors(self):
+ """
+ Incorrect bulk create serialization should return errors.
+ """
+
+ data = [
+ {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }, {
+ 'id': 1,
+ 'title': 'If this is a man',
+ 'author': 'Primo Levi'
+ }, {
+ 'id': 'foo',
+ 'title': 'The wind-up bird chronicle',
+ 'author': 'Haruki Murakami'
+ }
+ ]
+ expected_errors = [
+ {},
+ {},
+ {'id': ['A valid integer is required.']}
+ ]
+
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_list_datatype(self):
+ """
+ Data containing list of incorrect data type should return errors.
+ """
+ data = ['foo', 'bar', 'baz']
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ text_type_string = six.text_type.__name__
+ message = 'Invalid data. Expected a dictionary, but got %s.' % text_type_string
+ expected_errors = [
+ {'non_field_errors': [message]},
+ {'non_field_errors': [message]},
+ {'non_field_errors': [message]}
+ ]
+
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_single_datatype(self):
+ """
+ Data containing a single incorrect data type should return errors.
+ """
+ data = 123
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "int".']}
+
+ self.assertEqual(serializer.errors, expected_errors)
+
+ def test_invalid_single_object(self):
+ """
+ Data containing only a single object, instead of a list of objects
+ should return errors.
+ """
+ data = {
+ 'id': 0,
+ 'title': 'The electric kool-aid acid test',
+ 'author': 'Tom Wolfe'
+ }
+ serializer = self.BookSerializer(data=data, many=True)
+ self.assertEqual(serializer.is_valid(), False)
+
+ expected_errors = {'non_field_errors': ['Expected a list of items but got type "dict".']}
+
+ self.assertEqual(serializer.errors, expected_errors)
diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py
new file mode 100644
index 00000000..35b68ae7
--- /dev/null
+++ b/tests/test_serializer_lists.py
@@ -0,0 +1,290 @@
+from rest_framework import serializers
+from django.utils.datastructures import MultiValueDict
+
+
+class BasicObject:
+ """
+ A mock object for testing serializer save behavior.
+ """
+ def __init__(self, **kwargs):
+ self._data = kwargs
+ for key, value in kwargs.items():
+ setattr(self, key, value)
+
+ def __eq__(self, other):
+ if self._data.keys() != other._data.keys():
+ return False
+ for key in self._data.keys():
+ if self._data[key] != other._data[key]:
+ return False
+ return True
+
+
+class TestListSerializer:
+ """
+ Tests for using a ListSerializer as a top-level serializer.
+ Note that this is in contrast to using ListSerializer as a field.
+ """
+
+ def setup(self):
+ class IntegerListSerializer(serializers.ListSerializer):
+ child = serializers.IntegerField()
+ self.Serializer = IntegerListSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of items should return a list of validated items.
+ """
+ input_data = ["123", "456"]
+ expected_output = [123, 456]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x] style ids.
+ """
+ input_data = MultiValueDict({"[0]": ["123"], "[1]": ["456"]})
+ expected_output = [123, 456]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestListSerializerContainingNestedSerializer:
+ """
+ Tests for using a ListSerializer containing another serializer.
+ """
+
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integer = serializers.IntegerField()
+ boolean = serializers.BooleanField()
+
+ def create(self, validated_data):
+ return BasicObject(**validated_data)
+
+ class ObjectListSerializer(serializers.ListSerializer):
+ child = TestSerializer()
+
+ self.Serializer = ObjectListSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of dictionaries should return a list of
+ validated dictionaries.
+ """
+ input_data = [
+ {"integer": "123", "boolean": "true"},
+ {"integer": "456", "boolean": "false"}
+ ]
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_create(self):
+ """
+ Creating from a list of dictionaries should return a list of objects.
+ """
+ input_data = [
+ {"integer": "123", "boolean": "true"},
+ {"integer": "456", "boolean": "false"}
+ ]
+ expected_output = [
+ BasicObject(integer=123, boolean=True),
+ BasicObject(integer=456, boolean=False),
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.save() == expected_output
+
+ def test_serialize(self):
+ """
+ Serialization of a list of objects should return a list of dictionaries.
+ """
+ input_objects = [
+ BasicObject(integer=123, boolean=True),
+ BasicObject(integer=456, boolean=False)
+ ]
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(input_objects)
+ assert serializer.data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "[0]integer": ["123"],
+ "[0]boolean": ["true"],
+ "[1]integer": ["456"],
+ "[1]boolean": ["false"]
+ })
+ expected_output = [
+ {"integer": 123, "boolean": True},
+ {"integer": 456, "boolean": False}
+ ]
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestNestedListSerializer:
+ """
+ Tests for using a ListSerializer as a field.
+ """
+
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integers = serializers.ListSerializer(child=serializers.IntegerField())
+ booleans = serializers.ListSerializer(child=serializers.BooleanField())
+
+ def create(self, validated_data):
+ return BasicObject(**validated_data)
+
+ self.Serializer = TestSerializer
+
+ def test_validate(self):
+ """
+ Validating a list of items should return a list of validated items.
+ """
+ input_data = {
+ "integers": ["123", "456"],
+ "booleans": ["true", "false"]
+ }
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_create(self):
+ """
+ Creation with a list of items return an object with an attribute that
+ is a list of items.
+ """
+ input_data = {
+ "integers": ["123", "456"],
+ "booleans": ["true", "false"]
+ }
+ expected_output = BasicObject(
+ integers=[123, 456],
+ booleans=[True, False]
+ )
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.save() == expected_output
+
+ def test_serialize(self):
+ """
+ Serialization of a list of items should return a list of items.
+ """
+ input_object = BasicObject(
+ integers=[123, 456],
+ booleans=[True, False]
+ )
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(input_object)
+ assert serializer.data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock list structures using [x]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "integers[0]": ["123"],
+ "integers[1]": ["456"],
+ "booleans[0]": ["true"],
+ "booleans[1]": ["false"]
+ })
+ expected_output = {
+ "integers": [123, 456],
+ "booleans": [True, False]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestNestedListOfListsSerializer:
+ def setup(self):
+ class TestSerializer(serializers.Serializer):
+ integers = serializers.ListSerializer(
+ child=serializers.ListSerializer(
+ child=serializers.IntegerField()
+ )
+ )
+ booleans = serializers.ListSerializer(
+ child=serializers.ListSerializer(
+ child=serializers.BooleanField()
+ )
+ )
+
+ self.Serializer = TestSerializer
+
+ def test_validate(self):
+ input_data = {
+ 'integers': [['123', '456'], ['789', '0']],
+ 'booleans': [['true', 'true'], ['false', 'true']]
+ }
+ expected_output = {
+ "integers": [[123, 456], [789, 0]],
+ "booleans": [[True, True], [False, True]]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+ def test_validate_html_input(self):
+ """
+ HTML input should be able to mock lists of lists using [x][y]
+ style prefixes.
+ """
+ input_data = MultiValueDict({
+ "integers[0][0]": ["123"],
+ "integers[0][1]": ["456"],
+ "integers[1][0]": ["789"],
+ "integers[1][1]": ["000"],
+ "booleans[0][0]": ["true"],
+ "booleans[0][1]": ["true"],
+ "booleans[1][0]": ["false"],
+ "booleans[1][1]": ["true"]
+ })
+ expected_output = {
+ "integers": [[123, 456], [789, 0]],
+ "booleans": [[True, True], [False, True]]
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_output
+
+
+class TestListSerializerClass:
+ """Tests for a custom list_serializer_class."""
+ def test_list_serializer_class_validate(self):
+ class CustomListSerializer(serializers.ListSerializer):
+ def validate(self, attrs):
+ raise serializers.ValidationError('Non field error')
+
+ class TestSerializer(serializers.Serializer):
+ class Meta:
+ list_serializer_class = CustomListSerializer
+
+ serializer = TestSerializer(data=[], many=True)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'non_field_errors': ['Non field error']}
diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py
new file mode 100644
index 00000000..f5e4b26a
--- /dev/null
+++ b/tests/test_serializer_nested.py
@@ -0,0 +1,40 @@
+from rest_framework import serializers
+
+
+class TestNestedSerializer:
+ def setup(self):
+ class NestedSerializer(serializers.Serializer):
+ one = serializers.IntegerField(max_value=10)
+ two = serializers.IntegerField(max_value=10)
+
+ class TestSerializer(serializers.Serializer):
+ nested = NestedSerializer()
+
+ self.Serializer = TestSerializer
+
+ def test_nested_validate(self):
+ input_data = {
+ 'nested': {
+ 'one': '1',
+ 'two': '2',
+ }
+ }
+ expected_data = {
+ 'nested': {
+ 'one': 1,
+ 'two': 2,
+ }
+ }
+ serializer = self.Serializer(data=input_data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == expected_data
+
+ def test_nested_serialize_empty(self):
+ expected_data = {
+ 'nested': {
+ 'one': None,
+ 'two': None
+ }
+ }
+ serializer = self.Serializer()
+ assert serializer.data == expected_data
diff --git a/tests/test_settings.py b/tests/test_settings.py
new file mode 100644
index 00000000..f2ff4ca1
--- /dev/null
+++ b/tests/test_settings.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.settings import APISettings
+
+
+class TestSettings(TestCase):
+ def test_import_error_message_maintained(self):
+ """
+ Make sure import errors are captured and raised sensibly.
+ """
+ settings = APISettings({
+ 'DEFAULT_RENDERER_CLASSES': [
+ 'tests.invalid_module.InvalidClassName'
+ ]
+ })
+ with self.assertRaises(ImportError):
+ settings.DEFAULT_RENDERER_CLASSES
diff --git a/tests/test_status.py b/tests/test_status.py
new file mode 100644
index 00000000..721a6e30
--- /dev/null
+++ b/tests/test_status.py
@@ -0,0 +1,33 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.status import (
+ is_informational, is_success, is_redirect, is_client_error, is_server_error
+)
+
+
+class TestStatus(TestCase):
+ def test_status_categories(self):
+ self.assertFalse(is_informational(99))
+ self.assertTrue(is_informational(100))
+ self.assertTrue(is_informational(199))
+ self.assertFalse(is_informational(200))
+
+ self.assertFalse(is_success(199))
+ self.assertTrue(is_success(200))
+ self.assertTrue(is_success(299))
+ self.assertFalse(is_success(300))
+
+ self.assertFalse(is_redirect(299))
+ self.assertTrue(is_redirect(300))
+ self.assertTrue(is_redirect(399))
+ self.assertFalse(is_redirect(400))
+
+ self.assertFalse(is_client_error(399))
+ self.assertTrue(is_client_error(400))
+ self.assertTrue(is_client_error(499))
+ self.assertFalse(is_client_error(500))
+
+ self.assertFalse(is_server_error(499))
+ self.assertTrue(is_server_error(500))
+ self.assertTrue(is_server_error(599))
+ self.assertFalse(is_server_error(600))
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
new file mode 100644
index 00000000..0cee91f1
--- /dev/null
+++ b/tests/test_templatetags.py
@@ -0,0 +1,75 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.test import APIRequestFactory
+from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links
+
+
+factory = APIRequestFactory()
+
+
+class TemplateTagTests(TestCase):
+
+ def test_add_query_param_with_non_latin_charactor(self):
+ # Ensure we don't double-escape non-latin characters
+ # that are present in the querystring.
+ # See #1314.
+ request = factory.get("/", {'q': '查询'})
+ json_url = add_query_param(request, "format", "json")
+ self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url)
+ self.assertIn("format=json", json_url)
+
+
+class Issue1386Tests(TestCase):
+ """
+ Covers #1386
+ """
+
+ def test_issue_1386(self):
+ """
+ Test function urlize_quoted_links with different args
+ """
+ correct_urls = [
+ "asdf.com",
+ "asdf.net",
+ "www.as_df.org",
+ "as.d8f.ghj8.gov",
+ ]
+ for i in correct_urls:
+ res = urlize_quoted_links(i)
+ self.assertNotEqual(res, i)
+ self.assertIn(i, res)
+
+ incorrect_urls = [
+ "mailto://asdf@fdf.com",
+ "asdf.netnet",
+ ]
+ for i in incorrect_urls:
+ res = urlize_quoted_links(i)
+ self.assertEqual(i, res)
+
+ # example from issue #1386, this shouldn't raise an exception
+ urlize_quoted_links("asdf:[/p]zxcv.com")
+
+
+class URLizerTests(TestCase):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ def _urlize_dict_check(self, data):
+ """
+ For all items in dict test assert that the value is urlized key
+ """
+ for original, urlized in data.items():
+ assert urlize_quoted_links(original, nofollow=False) == urlized
+
+ def test_json_with_url(self):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ data = {}
+ data['"url": "http://api/users/1/", '] = \
+ '&quot;url&quot;: &quot;<a href="http://api/users/1/">http://api/users/1/</a>&quot;, '
+ data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
+ '&quot;foo_set&quot;: [\n &quot;<a href="http://api/foos/1/">http://api/foos/1/</a>&quot;\n], '
+ self._urlize_dict_check(data)
diff --git a/tests/test_testing.py b/tests/test_testing.py
new file mode 100644
index 00000000..87d2b61f
--- /dev/null
+++ b/tests/test_testing.py
@@ -0,0 +1,234 @@
+# encoding: utf-8
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url
+from django.contrib.auth.models import User
+from django.shortcuts import redirect
+from django.test import TestCase
+from rest_framework.decorators import api_view
+from rest_framework.response import Response
+from rest_framework.test import APIClient, APIRequestFactory, force_authenticate
+from io import BytesIO
+
+
+@api_view(['GET', 'POST'])
+def view(request):
+ return Response({
+ 'auth': request.META.get('HTTP_AUTHORIZATION', b''),
+ 'user': request.user.username
+ })
+
+
+@api_view(['GET', 'POST'])
+def session_view(request):
+ active_session = request.session.get('active_session', False)
+ request.session['active_session'] = True
+ return Response({
+ 'active_session': active_session
+ })
+
+
+@api_view(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'])
+def redirect_view(request):
+ return redirect('/view/')
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^view/$', view),
+ url(r'^session-view/$', session_view),
+ url(r'^redirect-view/$', redirect_view),
+)
+
+
+class TestAPITestClient(TestCase):
+ urls = 'tests.test_testing'
+
+ def setUp(self):
+ self.client = APIClient()
+
+ def test_credentials(self):
+ """
+ Setting `.credentials()` adds the required headers to each request.
+ """
+ self.client.credentials(HTTP_AUTHORIZATION='example')
+ for _ in range(0, 3):
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], 'example')
+
+ def test_force_authenticate(self):
+ """
+ Setting `.force_authenticate()` forcibly authenticates each request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ self.client.force_authenticate(user)
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], 'example')
+
+ def test_force_authenticate_with_sessions(self):
+ """
+ Setting `.force_authenticate()` forcibly authenticates each request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ self.client.force_authenticate(user)
+
+ # First request does not yet have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
+ # Subsequant requests have an active session
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], True)
+
+ # Force authenticating as `None` should also logout the user session.
+ self.client.force_authenticate(None)
+ response = self.client.get('/session-view/')
+ self.assertEqual(response.data['active_session'], False)
+
+ def test_csrf_exempt_by_default(self):
+ """
+ By default, the test client is CSRF exempt.
+ """
+ User.objects.create_user('example', 'example@example.com', 'password')
+ self.client.login(username='example', password='password')
+ response = self.client.post('/view/')
+ self.assertEqual(response.status_code, 200)
+
+ def test_explicitly_enforce_csrf_checks(self):
+ """
+ The test client can enforce CSRF checks.
+ """
+ client = APIClient(enforce_csrf_checks=True)
+ User.objects.create_user('example', 'example@example.com', 'password')
+ client.login(username='example', password='password')
+ response = client.post('/view/')
+ expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(response.data, expected)
+
+ def test_can_logout(self):
+ """
+ `logout()` resets stored credentials
+ """
+ self.client.credentials(HTTP_AUTHORIZATION='example')
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], 'example')
+ self.client.logout()
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], b'')
+
+ def test_logout_resets_force_authenticate(self):
+ """
+ `logout()` resets any `force_authenticate`
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ self.client.force_authenticate(user)
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], 'example')
+ self.client.logout()
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['user'], '')
+
+ def test_follow_redirect(self):
+ """
+ Follow redirect by setting follow argument.
+ """
+ response = self.client.get('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.get('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.post('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.post('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.put('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.put('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.patch('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.patch('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.delete('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.delete('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+ response = self.client.options('/redirect-view/')
+ self.assertEqual(response.status_code, 302)
+ response = self.client.options('/redirect-view/', follow=True)
+ self.assertIsNotNone(response.redirect_chain)
+ self.assertEqual(response.status_code, 200)
+
+
+class TestAPIRequestFactory(TestCase):
+ def test_csrf_exempt_by_default(self):
+ """
+ By default, the test client is CSRF exempt.
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ factory = APIRequestFactory()
+ request = factory.post('/view/')
+ request.user = user
+ response = view(request)
+ self.assertEqual(response.status_code, 200)
+
+ def test_explicitly_enforce_csrf_checks(self):
+ """
+ The test client can enforce CSRF checks.
+ """
+ user = User.objects.create_user('example', 'example@example.com', 'password')
+ factory = APIRequestFactory(enforce_csrf_checks=True)
+ request = factory.post('/view/')
+ request.user = user
+ response = view(request)
+ expected = {'detail': 'CSRF Failed: CSRF cookie not set.'}
+ self.assertEqual(response.status_code, 403)
+ self.assertEqual(response.data, expected)
+
+ def test_invalid_format(self):
+ """
+ Attempting to use a format that is not configured will raise an
+ assertion error.
+ """
+ factory = APIRequestFactory()
+ self.assertRaises(
+ AssertionError, factory.post,
+ path='/view/', data={'example': 1}, format='xml'
+ )
+
+ def test_force_authenticate(self):
+ """
+ Setting `force_authenticate()` forcibly authenticates the request.
+ """
+ user = User.objects.create_user('example', 'example@example.com')
+ factory = APIRequestFactory()
+ request = factory.get('/view')
+ force_authenticate(request, user=user)
+ response = view(request)
+ self.assertEqual(response.data['user'], 'example')
+
+ def test_upload_file(self):
+ # This is a 1x1 black png
+ simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82')
+ simple_png.name = 'test.png'
+ factory = APIRequestFactory()
+ factory.post('/', data={'image': simple_png})
+
+ def test_request_factory_url_arguments(self):
+ """
+ This is a non regression test against #1461
+ """
+ factory = APIRequestFactory()
+ request = factory.get('/view/?demo=test')
+ self.assertEqual(dict(request.GET), {'demo': ['test']})
+ request = factory.get('/view/', {'demo': 'test'})
+ self.assertEqual(dict(request.GET), {'demo': ['test']})
diff --git a/rest_framework/tests/test_throttling.py b/tests/test_throttling.py
index 19bc691a..50a53b3e 100644
--- a/rest_framework/tests/test_throttling.py
+++ b/tests/test_throttling.py
@@ -5,9 +5,10 @@ from __future__ import unicode_literals
from django.test import TestCase
from django.contrib.auth.models import User
from django.core.cache import cache
+from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView
-from rest_framework.throttling import UserRateThrottle, ScopedRateThrottle
+from rest_framework.throttling import BaseThrottle, UserRateThrottle, ScopedRateThrottle
from rest_framework.response import Response
@@ -21,6 +22,14 @@ class User3MinRateThrottle(UserRateThrottle):
scope = 'minutes'
+class NonTimeThrottle(BaseThrottle):
+ def allow_request(self, request, view):
+ if not hasattr(self.__class__, 'called'):
+ self.__class__.called = True
+ return True
+ return False
+
+
class MockView(APIView):
throttle_classes = (User3SecRateThrottle,)
@@ -35,6 +44,13 @@ class MockView_MinuteThrottling(APIView):
return Response('foo')
+class MockView_NonTimeThrottling(APIView):
+ throttle_classes = (NonTimeThrottle,)
+
+ def get(self, request):
+ return Response('foo')
+
+
class ThrottlingTests(TestCase):
def setUp(self):
"""
@@ -93,7 +109,7 @@ class ThrottlingTests(TestCase):
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
- Ensure the response returns an X-Throttle field with status and next attributes
+ Ensure the response returns an Retry-After field with status and next attributes
set properly.
"""
request = self.factory.get('/')
@@ -101,44 +117,66 @@ class ThrottlingTests(TestCase):
self.set_throttle_timer(view, timer)
response = view.as_view()(request)
if expect is not None:
- self.assertEqual(response['X-Throttle-Wait-Seconds'], expect)
+ self.assertEqual(response['Retry-After'], expect)
else:
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
+ self.assertFalse('Retry-After' in response)
def test_seconds_fields(self):
"""
Ensure for second based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView,
- ((0, None),
- (0, None),
- (0, None),
- (0, '1')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '1')
+ )
+ )
def test_minutes_fields(self):
"""
Ensure for minute based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (0, None),
- (0, None),
- (0, '60')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '60')
+ )
+ )
def test_next_rate_remains_constant_if_followed(self):
"""
If a client follows the recommended next request rate,
the throttling rate should stay constant.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (20, None),
- (40, None),
- (60, None),
- (80, None)
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (20, None),
+ (40, None),
+ (60, None),
+ (80, None)
+ )
+ )
+
+ def test_non_time_throttle(self):
+ """
+ Ensure for second based throttles.
+ """
+ request = self.factory.get('/')
+
+ self.assertFalse(hasattr(MockView_NonTimeThrottling.throttle_classes[0], 'called'))
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('Retry-After' in response)
+
+ self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
+
+ response = MockView_NonTimeThrottling.as_view()(request)
+ self.assertFalse('Retry-After' in response)
class ScopedRateThrottleTests(TestCase):
@@ -150,7 +188,9 @@ class ScopedRateThrottleTests(TestCase):
class XYScopedRateThrottle(ScopedRateThrottle):
TIMER_SECONDS = 0
THROTTLE_RATES = {'x': '3/min', 'y': '1/min'}
- timer = lambda self: self.TIMER_SECONDS
+
+ def timer(self):
+ return self.TIMER_SECONDS
class XView(APIView):
throttle_classes = (XYScopedRateThrottle,)
@@ -244,3 +284,70 @@ class ScopedRateThrottleTests(TestCase):
self.increment_timer()
response = self.unscoped_view(request)
self.assertEqual(200, response.status_code)
+
+
+class XffTestingBase(TestCase):
+ def setUp(self):
+
+ class Throttle(ScopedRateThrottle):
+ THROTTLE_RATES = {'test_limit': '1/day'}
+ TIMER_SECONDS = 0
+
+ def timer(self):
+ return self.TIMER_SECONDS
+
+ class View(APIView):
+ throttle_classes = (Throttle,)
+ throttle_scope = 'test_limit'
+
+ def get(self, request):
+ return Response('test_limit')
+
+ cache.clear()
+ self.throttle = Throttle()
+ self.view = View.as_view()
+ self.request = APIRequestFactory().get('/some_uri')
+ self.request.META['REMOTE_ADDR'] = '3.3.3.3'
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 2.2.2.2'
+
+ def config_proxy(self, num_proxies):
+ setattr(api_settings, 'NUM_PROXIES', num_proxies)
+
+
+class IdWithXffBasicTests(XffTestingBase):
+ def test_accepts_request_under_limit(self):
+ self.config_proxy(0)
+ self.assertEqual(200, self.view(self.request).status_code)
+
+ def test_denies_request_over_limit(self):
+ self.config_proxy(0)
+ self.view(self.request)
+ self.assertEqual(429, self.view(self.request).status_code)
+
+
+class XffSpoofingTests(XffTestingBase):
+ def test_xff_spoofing_doesnt_change_machine_id_with_one_app_proxy(self):
+ self.config_proxy(1)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 5.5.5.5, 2.2.2.2'
+ self.assertEqual(429, self.view(self.request).status_code)
+
+ def test_xff_spoofing_doesnt_change_machine_id_with_two_app_proxies(self):
+ self.config_proxy(2)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '4.4.4.4, 1.1.1.1, 2.2.2.2'
+ self.assertEqual(429, self.view(self.request).status_code)
+
+
+class XffUniqueMachinesTest(XffTestingBase):
+ def test_unique_clients_are_counted_independently_with_one_proxy(self):
+ self.config_proxy(1)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 1.1.1.1, 7.7.7.7'
+ self.assertEqual(200, self.view(self.request).status_code)
+
+ def test_unique_clients_are_counted_independently_with_two_proxies(self):
+ self.config_proxy(2)
+ self.view(self.request)
+ self.request.META['HTTP_X_FORWARDED_FOR'] = '0.0.0.0, 7.7.7.7, 2.2.2.2'
+ self.assertEqual(200, self.view(self.request).status_code)
diff --git a/rest_framework/tests/test_urlpatterns.py b/tests/test_urlpatterns.py
index 8132ec4c..e0060e69 100644
--- a/rest_framework/tests/test_urlpatterns.py
+++ b/tests/test_urlpatterns.py
@@ -1,9 +1,9 @@
from __future__ import unicode_literals
from collections import namedtuple
+from django.conf.urls import patterns, url, include
from django.core import urlresolvers
from django.test import TestCase
from rest_framework.test import APIRequestFactory
-from rest_framework.compat import patterns, url, include
from rest_framework.urlpatterns import format_suffix_patterns
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 00000000..8c286ea4
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,166 @@
+from __future__ import unicode_literals
+from django.core.exceptions import ImproperlyConfigured
+from django.conf.urls import patterns, url
+from django.test import TestCase
+from django.utils import six
+from rest_framework.utils.model_meta import _resolve_model
+from rest_framework.utils.breadcrumbs import get_breadcrumbs
+from rest_framework.views import APIView
+from tests.models import BasicModel
+
+import rest_framework.utils.model_meta
+
+
+class Root(APIView):
+ pass
+
+
+class ResourceRoot(APIView):
+ pass
+
+
+class ResourceInstance(APIView):
+ pass
+
+
+class NestedResourceRoot(APIView):
+ pass
+
+
+class NestedResourceInstance(APIView):
+ pass
+
+
+urlpatterns = patterns(
+ '',
+ url(r'^$', Root.as_view()),
+ url(r'^resource/$', ResourceRoot.as_view()),
+ url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
+ url(r'^resource/(?P<key>[0-9]+)/$', NestedResourceRoot.as_view()),
+ url(r'^resource/(?P<key>[0-9]+)/(?P<other>[A-Za-z]+)$', NestedResourceInstance.as_view()),
+)
+
+
+class BreadcrumbTests(TestCase):
+ """
+ Tests the breadcrumb functionality used by the HTML renderer.
+ """
+ urls = 'tests.test_utils'
+
+ def test_root_breadcrumbs(self):
+ url = '/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+ def test_resource_root_breadcrumbs(self):
+ url = '/resource/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/')
+ ]
+ )
+
+ def test_resource_instance_breadcrumbs(self):
+ url = '/resource/123'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123')
+ ]
+ )
+
+ def test_nested_resource_breadcrumbs(self):
+ url = '/resource/123/'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/')
+ ]
+ )
+
+ def test_nested_resource_instance_breadcrumbs(self):
+ url = '/resource/123/abc'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/'),
+ ('Nested Resource Instance', '/resource/123/abc')
+ ]
+ )
+
+ def test_broken_url_breadcrumbs_handled_gracefully(self):
+ url = '/foobar'
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
+
+
+class ResolveModelTests(TestCase):
+ """
+ `_resolve_model` should return a Django model class given the
+ provided argument is a Django model class itself, or a properly
+ formatted string representation of one.
+ """
+ def test_resolve_django_model(self):
+ resolved_model = _resolve_model(BasicModel)
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_string_representation(self):
+ resolved_model = _resolve_model('tests.BasicModel')
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
+
+ def test_resolve_non_django_model(self):
+ with self.assertRaises(ValueError):
+ _resolve_model(TestCase)
+
+ def test_resolve_improper_string_representation(self):
+ with self.assertRaises(ValueError):
+ _resolve_model('BasicModel')
+
+
+class ResolveModelWithPatchedDjangoTests(TestCase):
+ """
+ Test coverage for when Django's `get_model` returns `None`.
+
+ Under certain circumstances Django may return `None` with `get_model`:
+ http://git.io/get-model-source
+
+ It usually happens with circular imports so it is important that DRF
+ excepts early, otherwise fault happens downstream and is much more
+ difficult to debug.
+
+ """
+
+ def setUp(self):
+ """Monkeypatch get_model."""
+ self.get_model = rest_framework.utils.model_meta.models.get_model
+
+ def get_model(app_label, model_name):
+ return None
+
+ rest_framework.utils.model_meta.models.get_model = get_model
+
+ def tearDown(self):
+ """Revert monkeypatching."""
+ rest_framework.utils.model_meta.models.get_model = self.get_model
+
+ def test_blows_up_if_model_does_not_resolve(self):
+ with self.assertRaises(ImproperlyConfigured):
+ _resolve_model('tests.BasicModel')
diff --git a/tests/test_validation.py b/tests/test_validation.py
new file mode 100644
index 00000000..4234efd3
--- /dev/null
+++ b/tests/test_validation.py
@@ -0,0 +1,183 @@
+from __future__ import unicode_literals
+from django.core.validators import RegexValidator, MaxValueValidator
+from django.db import models
+from django.test import TestCase
+from rest_framework import generics, serializers, status
+from rest_framework.test import APIRequestFactory
+import re
+
+factory = APIRequestFactory()
+
+
+# Regression for #666
+
+class ValidationModel(models.Model):
+ blank_validated_field = models.CharField(max_length=255)
+
+
+class ValidationModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ValidationModel
+ fields = ('blank_validated_field',)
+ read_only_fields = ('blank_validated_field',)
+
+
+class UpdateValidationModel(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ValidationModel.objects.all()
+ serializer_class = ValidationModelSerializer
+
+
+# Regression for #653
+
+class ShouldValidateModel(models.Model):
+ should_validate_field = models.CharField(max_length=255)
+
+
+class ShouldValidateModelSerializer(serializers.ModelSerializer):
+ renamed = serializers.CharField(source='should_validate_field', required=False)
+
+ def validate_renamed(self, value):
+ if len(value) < 3:
+ raise serializers.ValidationError('Minimum 3 characters.')
+ return value
+
+ class Meta:
+ model = ShouldValidateModel
+ fields = ('renamed',)
+
+
+class TestPreSaveValidationExclusionsSerializer(TestCase):
+ def test_renamed_fields_are_model_validated(self):
+ """
+ Ensure fields with 'source' applied do get still get model validation.
+ """
+ # We've set `required=False` on the serializer, but the model
+ # does not have `blank=True`, so this serializer should not validate.
+ serializer = ShouldValidateModelSerializer(data={'renamed': ''})
+ self.assertEqual(serializer.is_valid(), False)
+ self.assertIn('renamed', serializer.errors)
+ self.assertNotIn('should_validate_field', serializer.errors)
+
+
+class TestCustomValidationMethods(TestCase):
+ def test_custom_validation_method_is_executed(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'})
+ self.assertFalse(serializer.is_valid())
+ self.assertIn('renamed', serializer.errors)
+
+ def test_custom_validation_method_passing(self):
+ serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'})
+ self.assertTrue(serializer.is_valid())
+
+
+class ValidationSerializer(serializers.Serializer):
+ foo = serializers.CharField()
+
+ def validate_foo(self, attrs, source):
+ raise serializers.ValidationError("foo invalid")
+
+ def validate(self, attrs):
+ raise serializers.ValidationError("serializer invalid")
+
+
+class TestAvoidValidation(TestCase):
+ """
+ If serializer was initialized with invalid data (None or non dict-like), it
+ should avoid validation layer (validate_<field> and validate methods)
+ """
+ def test_serializer_errors_has_only_invalid_data_error(self):
+ serializer = ValidationSerializer(data='invalid data')
+ self.assertFalse(serializer.is_valid())
+ self.assertDictEqual(serializer.errors, {
+ 'non_field_errors': [
+ 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__
+ ]
+ })
+
+
+# regression tests for issue: 1493
+
+class ValidationMaxValueValidatorModel(models.Model):
+ number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)])
+
+
+class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ValidationMaxValueValidatorModel
+
+
+class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView):
+ queryset = ValidationMaxValueValidatorModel.objects.all()
+ serializer_class = ValidationMaxValueValidatorModelSerializer
+
+
+class TestMaxValueValidatorValidation(TestCase):
+
+ def test_max_value_validation_serializer_success(self):
+ serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99})
+ self.assertTrue(serializer.is_valid())
+
+ def test_max_value_validation_serializer_fails(self):
+ serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101})
+ self.assertFalse(serializer.is_valid())
+ self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors)
+
+ def test_max_value_validation_success(self):
+ obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
+ request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json')
+ view = UpdateMaxValueValidationModel().as_view()
+ response = view(request, pk=obj.pk).render()
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+
+ def test_max_value_validation_fail(self):
+ obj = ValidationMaxValueValidatorModel.objects.create(number_value=100)
+ request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json')
+ view = UpdateMaxValueValidationModel().as_view()
+ response = view(request, pk=obj.pk).render()
+ self.assertEqual(response.content, b'{"number_value":["Ensure this value is less than or equal to 100."]}')
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestChoiceFieldChoicesValidate(TestCase):
+ CHOICES = [
+ (0, 'Small'),
+ (1, 'Medium'),
+ (2, 'Large'),
+ ]
+
+ CHOICES_NESTED = [
+ ('Category', (
+ (1, 'First'),
+ (2, 'Second'),
+ (3, 'Third'),
+ )),
+ (4, 'Fourth'),
+ ]
+
+ def test_choices(self):
+ """
+ Make sure a value for choices works as expected.
+ """
+ f = serializers.ChoiceField(choices=self.CHOICES)
+ value = self.CHOICES[0][0]
+ try:
+ f.to_internal_value(value)
+ except serializers.ValidationError:
+ self.fail("Value %s does not validate" % str(value))
+
+
+class RegexSerializer(serializers.Serializer):
+ pin = serializers.CharField(
+ validators=[RegexValidator(regex=re.compile('^[0-9]{4,6}$'),
+ message='A PIN is 4-6 digits')])
+
+expected_repr = """
+RegexSerializer():
+ pin = CharField(validators=[<django.core.validators.RegexValidator object>])
+""".strip()
+
+
+class TestRegexSerializer(TestCase):
+ def test_regex_repr(self):
+ serializer_repr = repr(RegexSerializer())
+ assert serializer_repr == expected_repr
diff --git a/tests/test_validators.py b/tests/test_validators.py
new file mode 100644
index 00000000..127ec6f8
--- /dev/null
+++ b/tests/test_validators.py
@@ -0,0 +1,347 @@
+from django.db import models
+from django.test import TestCase
+from rest_framework import serializers
+import datetime
+
+
+def dedent(blocktext):
+ return '\n'.join([line[12:] for line in blocktext.splitlines()[1:-1]])
+
+
+# Tests for `UniqueValidator`
+# ---------------------------
+
+class UniquenessModel(models.Model):
+ username = models.CharField(unique=True, max_length=100)
+
+
+class UniquenessSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessModel
+
+
+class AnotherUniquenessModel(models.Model):
+ code = models.IntegerField(unique=True)
+
+
+class AnotherUniquenessSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = AnotherUniquenessModel
+
+
+class TestUniquenessValidation(TestCase):
+ def setUp(self):
+ self.instance = UniquenessModel.objects.create(username='existing')
+
+ def test_repr(self):
+ serializer = UniquenessSerializer()
+ expected = dedent("""
+ UniquenessSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ username = CharField(max_length=100, validators=[<UniqueValidator(queryset=UniquenessModel.objects.all())>])
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique(self):
+ data = {'username': 'existing'}
+ serializer = UniquenessSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {'username': ['This field must be unique.']}
+
+ def test_is_unique(self):
+ data = {'username': 'other'}
+ serializer = UniquenessSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'other'}
+
+ def test_updated_instance_excluded(self):
+ data = {'username': 'existing'}
+ serializer = UniquenessSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'username': 'existing'}
+
+ def test_doesnt_pollute_model(self):
+ instance = AnotherUniquenessModel.objects.create(code='100')
+ serializer = AnotherUniquenessSerializer(instance)
+ self.assertEqual(
+ AnotherUniquenessModel._meta.get_field('code').validators, [])
+
+ # Accessing data shouldn't effect validators on the model
+ serializer.data
+ self.assertEqual(
+ AnotherUniquenessModel._meta.get_field('code').validators, [])
+
+
+# Tests for `UniqueTogetherValidator`
+# -----------------------------------
+
+class UniquenessTogetherModel(models.Model):
+ race_name = models.CharField(max_length=100)
+ position = models.IntegerField()
+
+ class Meta:
+ unique_together = ('race_name', 'position')
+
+
+class NullUniquenessTogetherModel(models.Model):
+ """
+ Used to ensure that null values are not included when checking
+ unique_together constraints.
+
+ Ignoring items which have a null in any of the validated fields is the same
+ behavior that database backends will use when they have the
+ unique_together constraint added.
+
+ Example case: a null position could indicate a non-finisher in the race,
+ there could be many non-finishers in a race, but all non-NULL
+ values *should* be unique against the given `race_name`.
+ """
+ date_of_birth = models.DateField(null=True) # Not part of the uniqueness constraint
+ race_name = models.CharField(max_length=100)
+ position = models.IntegerField(null=True)
+
+ class Meta:
+ unique_together = ('race_name', 'position')
+
+
+class UniquenessTogetherSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+
+
+class NullUniquenessTogetherSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = NullUniquenessTogetherModel
+
+
+class TestUniquenessTogetherValidation(TestCase):
+ def setUp(self):
+ self.instance = UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=1
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='example',
+ position=2
+ )
+ UniquenessTogetherModel.objects.create(
+ race_name='other',
+ position=1
+ )
+
+ def test_repr(self):
+ serializer = UniquenessTogetherSerializer()
+ expected = dedent("""
+ UniquenessTogetherSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100, required=True)
+ position = IntegerField(required=True)
+ class Meta:
+ validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('race_name', 'position'))>]
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique_together(self):
+ """
+ Failing unique together validation should result in non field errors.
+ """
+ data = {'race_name': 'example', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'non_field_errors': [
+ 'The fields race_name, position must make a unique set.'
+ ]
+ }
+
+ def test_is_unique_together(self):
+ """
+ In a unique together validation, one field may be non-unique
+ so long as the set as a whole is unique.
+ """
+ data = {'race_name': 'other', 'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'other',
+ 'position': 2
+ }
+
+ def test_updated_instance_excluded_from_unique_together(self):
+ """
+ When performing an update, the existing instance does not count
+ as a match against uniqueness.
+ """
+ data = {'race_name': 'example', 'position': 1}
+ serializer = UniquenessTogetherSerializer(self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'race_name': 'example',
+ 'position': 1
+ }
+
+ def test_unique_together_is_required(self):
+ """
+ In a unique together validation, all fields are required.
+ """
+ data = {'position': 2}
+ serializer = UniquenessTogetherSerializer(data=data, partial=True)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'race_name': ['This field is required.']
+ }
+
+ def test_ignore_excluded_fields(self):
+ """
+ When model fields are not included in a serializer, then uniqueness
+ validators should not be added for that field.
+ """
+ class ExcludedFieldSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniquenessTogetherModel
+ fields = ('id', 'race_name',)
+ serializer = ExcludedFieldSerializer()
+ expected = dedent("""
+ ExcludedFieldSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ race_name = CharField(max_length=100)
+ """)
+ assert repr(serializer) == expected
+
+ def test_ignore_validation_for_null_fields(self):
+ # None values that are on fields which are part of the uniqueness
+ # constraint cause the instance to ignore uniqueness validation.
+ NullUniquenessTogetherModel.objects.create(
+ date_of_birth=datetime.date(2000, 1, 1),
+ race_name='Paris Marathon',
+ position=None
+ )
+ data = {
+ 'date': datetime.date(2000, 1, 1),
+ 'race_name': 'Paris Marathon',
+ 'position': None
+ }
+ serializer = NullUniquenessTogetherSerializer(data=data)
+ assert serializer.is_valid()
+
+ def test_do_not_ignore_validation_for_null_fields(self):
+ # None values that are not on fields part of the uniqueness constraint
+ # do not cause the instance to skip validation.
+ NullUniquenessTogetherModel.objects.create(
+ date_of_birth=datetime.date(2000, 1, 1),
+ race_name='Paris Marathon',
+ position=1
+ )
+ data = {'date': None, 'race_name': 'Paris Marathon', 'position': 1}
+ serializer = NullUniquenessTogetherSerializer(data=data)
+ assert not serializer.is_valid()
+
+
+# Tests for `UniqueForDateValidator`
+# ----------------------------------
+
+class UniqueForDateModel(models.Model):
+ slug = models.CharField(max_length=100, unique_for_date='published')
+ published = models.DateField()
+
+
+class UniqueForDateSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = UniqueForDateModel
+
+
+class TestUniquenessForDateValidation(TestCase):
+ def setUp(self):
+ self.instance = UniqueForDateModel.objects.create(
+ slug='existing',
+ published='2000-01-01'
+ )
+
+ def test_repr(self):
+ serializer = UniqueForDateSerializer()
+ expected = dedent("""
+ UniqueForDateSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = DateField(required=True)
+ class Meta:
+ validators = [<UniqueForDateValidator(queryset=UniqueForDateModel.objects.all(), field='slug', date_field='published')>]
+ """)
+ assert repr(serializer) == expected
+
+ def test_is_not_unique_for_date(self):
+ """
+ Failing unique for date validation should result in field error.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-01'}
+ serializer = UniqueForDateSerializer(data=data)
+ assert not serializer.is_valid()
+ assert serializer.errors == {
+ 'slug': ['This field must be unique for the "published" date.']
+ }
+
+ def test_is_unique_for_date(self):
+ """
+ Passing unique for date validation.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-02'}
+ serializer = UniqueForDateSerializer(data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'slug': 'existing',
+ 'published': datetime.date(2000, 1, 2)
+ }
+
+ def test_updated_instance_excluded_from_unique_for_date(self):
+ """
+ When performing an update, the existing instance does not count
+ as a match against unique_for_date.
+ """
+ data = {'slug': 'existing', 'published': '2000-01-01'}
+ serializer = UniqueForDateSerializer(instance=self.instance, data=data)
+ assert serializer.is_valid()
+ assert serializer.validated_data == {
+ 'slug': 'existing',
+ 'published': datetime.date(2000, 1, 1)
+ }
+
+
+class HiddenFieldUniqueForDateModel(models.Model):
+ slug = models.CharField(max_length=100, unique_for_date='published')
+ published = models.DateTimeField(auto_now_add=True)
+
+
+class TestHiddenFieldUniquenessForDateValidation(TestCase):
+ def test_repr_date_field_not_included(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = HiddenFieldUniqueForDateModel
+ fields = ('id', 'slug')
+
+ serializer = TestSerializer()
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = HiddenField(default=CreateOnlyDefault(<function now>))
+ class Meta:
+ validators = [<UniqueForDateValidator(queryset=HiddenFieldUniqueForDateModel.objects.all(), field='slug', date_field='published')>]
+ """)
+ assert repr(serializer) == expected
+
+ def test_repr_date_field_included(self):
+ class TestSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = HiddenFieldUniqueForDateModel
+ fields = ('id', 'slug', 'published')
+
+ serializer = TestSerializer()
+ expected = dedent("""
+ TestSerializer():
+ id = IntegerField(label='ID', read_only=True)
+ slug = CharField(max_length=100)
+ published = DateTimeField(default=CreateOnlyDefault(<function now>), read_only=True)
+ class Meta:
+ validators = [<UniqueForDateValidator(queryset=HiddenFieldUniqueForDateModel.objects.all(), field='slug', date_field='published')>]
+ """)
+ assert repr(serializer) == expected
diff --git a/tests/test_versioning.py b/tests/test_versioning.py
new file mode 100644
index 00000000..90ad8afd
--- /dev/null
+++ b/tests/test_versioning.py
@@ -0,0 +1,264 @@
+from .utils import UsingURLPatterns
+from django.conf.urls import include, url
+from rest_framework import serializers
+from rest_framework import status, versioning
+from rest_framework.decorators import APIView
+from rest_framework.response import Response
+from rest_framework.reverse import reverse
+from rest_framework.test import APIRequestFactory, APITestCase
+from rest_framework.versioning import NamespaceVersioning
+import pytest
+
+
+class RequestVersionView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+
+class ReverseView(APIView):
+ def get(self, request, *args, **kwargs):
+ return Response({'url': reverse('another', request=request)})
+
+
+class RequestInvalidVersionView(APIView):
+ def determine_version(self, request, *args, **kwargs):
+ scheme = self.versioning_class()
+ scheme.allowed_versions = ('v1', 'v2')
+ return (scheme.determine_version(request, *args, **kwargs), scheme)
+
+ def get(self, request, *args, **kwargs):
+ return Response({'version': request.version})
+
+
+factory = APIRequestFactory()
+
+
+def dummy_view(request):
+ pass
+
+
+def dummy_pk_view(request, pk):
+ pass
+
+
+class TestRequestVersion:
+ def test_unversioned(self):
+ view = RequestVersionView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=1.2.3')
+ response = view(request)
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/1.2.3/endpoint/')
+ response = view(request, version='1.2.3')
+ assert response.data == {'version': '1.2.3'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+ def test_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'version': 'v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'version': None}
+
+
+class TestURLReversing(UsingURLPatterns, APITestCase):
+ included = [
+ url(r'^namespaced/$', dummy_view, name='another'),
+ url(r'^example/(?P<pk>\d+)/$', dummy_pk_view, name='example-detail')
+ ]
+
+ urlpatterns = [
+ url(r'^v1/', include(included, namespace='v1')),
+ url(r'^another/$', dummy_view, name='another'),
+ url(r'^(?P<version>[^/]+)/another/$', dummy_view, name='another'),
+ ]
+
+ def test_reverse_unversioned(self):
+ view = ReverseView.as_view()
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v1')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/?version=v1'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v1.example.org')
+ response = view(request)
+ assert response.data == {'url': 'http://v1.example.org/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/another/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+ def test_reverse_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v1'
+
+ scheme = versioning.NamespaceVersioning
+ view = ReverseView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v1/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v1')
+ assert response.data == {'url': 'http://testserver/v1/namespaced/'}
+
+ request = factory.get('/endpoint/')
+ response = view(request)
+ assert response.data == {'url': 'http://testserver/another/'}
+
+
+class TestInvalidVersion:
+ def test_invalid_query_param_versioning(self):
+ scheme = versioning.QueryParameterVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/?version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_host_name_versioning(self):
+ scheme = versioning.HostNameVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_HOST='v3.example.org')
+ response = view(request)
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_accept_header_versioning(self):
+ scheme = versioning.AcceptHeaderVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/endpoint/', HTTP_ACCEPT='application/json; version=v3')
+ response = view(request)
+ assert response.status_code == status.HTTP_406_NOT_ACCEPTABLE
+
+ def test_invalid_url_path_versioning(self):
+ scheme = versioning.URLPathVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+ def test_invalid_namespace_versioning(self):
+ class FakeResolverMatch:
+ namespace = 'v3'
+
+ scheme = versioning.NamespaceVersioning
+ view = RequestInvalidVersionView.as_view(versioning_class=scheme)
+
+ request = factory.get('/v3/endpoint/')
+ request.resolver_match = FakeResolverMatch
+ response = view(request, version='v3')
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+
+
+class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase):
+ included = [
+ url(r'^namespaced/(?P<pk>\d+)/$', dummy_view, name='namespaced'),
+ ]
+
+ urlpatterns = [
+ url(r'^v1/', include(included, namespace='v1')),
+ url(r'^v2/', include(included, namespace='v2'))
+ ]
+
+ def setUp(self):
+ super(TestHyperlinkedRelatedField, self).setUp()
+
+ class MockQueryset(object):
+ def get(self, pk):
+ return 'object %s' % pk
+
+ self.field = serializers.HyperlinkedRelatedField(
+ view_name='namespaced',
+ queryset=MockQueryset()
+ )
+ request = factory.get('/')
+ request.versioning_scheme = NamespaceVersioning()
+ request.version = 'v1'
+ self.field._context = {'request': request}
+
+ def test_bug_2489(self):
+ assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3'
+ with pytest.raises(serializers.ValidationError):
+ self.field.to_internal_value('/v2/namespaced/3/')
diff --git a/rest_framework/tests/test_views.py b/tests/test_views.py
index c0bec5ae..77b113ee 100644
--- a/rest_framework/tests/test_views.py
+++ b/tests/test_views.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+import sys
import copy
from django.test import TestCase
from rest_framework import status
@@ -11,6 +12,11 @@ from rest_framework.views import APIView
factory = APIRequestFactory()
+if sys.version_info[:2] >= (3, 4):
+ JSON_ERROR = 'JSON parse error - Expecting value:'
+else:
+ JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
+
class BasicView(APIView):
def get(self, request, *args, **kwargs):
@@ -32,13 +38,23 @@ def basic_view(request):
return {'method': 'PATCH', 'data': request.DATA}
+class ErrorView(APIView):
+ def get(self, request, *args, **kwargs):
+ raise Exception
+
+
+@api_view(['GET'])
+def error_view(request):
+ raise Exception
+
+
def sanitise_json_error(error_dict):
"""
Exact contents of JSON error messages depend on the installed version
of json.
"""
ret = copy.copy(error_dict)
- chop = len('JSON parse error - No JSON object could be decoded')
+ chop = len(JSON_ERROR)
ret['detail'] = ret['detail'][:chop]
return ret
@@ -51,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -66,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -80,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -95,7 +111,38 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
+
+
+class TestCustomExceptionHandler(TestCase):
+ def setUp(self):
+ self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER
+
+ def exception_handler(exc):
+ return Response('Error!', status=status.HTTP_400_BAD_REQUEST)
+
+ api_settings.EXCEPTION_HANDLER = exception_handler
+
+ def tearDown(self):
+ api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER
+
+ def test_class_based_view_exception_handler(self):
+ view = ErrorView.as_view()
+
+ request = factory.get('/', content_type='application/json')
+ response = view(request)
+ expected = 'Error!'
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data, expected)
+
+ def test_function_based_view_exception_handler(self):
+ view = error_view
+
+ request = factory.get('/', content_type='application/json')
+ response = view(request)
+ expected = 'Error!'
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+ self.assertEqual(response.data, expected)
diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py
new file mode 100644
index 00000000..4d18a955
--- /dev/null
+++ b/tests/test_viewsets.py
@@ -0,0 +1,35 @@
+from django.test import TestCase
+from rest_framework import status
+from rest_framework.response import Response
+from rest_framework.test import APIRequestFactory
+from rest_framework.viewsets import GenericViewSet
+
+
+factory = APIRequestFactory()
+
+
+class BasicViewSet(GenericViewSet):
+ def list(self, request, *args, **kwargs):
+ return Response({'ACTION': 'LIST'})
+
+
+class InitializeViewSetsTestCase(TestCase):
+ def test_initialize_view_set_with_actions(self):
+ request = factory.get('/', '', content_type='application/json')
+ my_view = BasicViewSet.as_view(actions={
+ 'get': 'list',
+ })
+
+ response = my_view(request)
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, {'ACTION': 'LIST'})
+
+ def test_initialize_view_set_with_empty_actions(self):
+ try:
+ BasicViewSet.as_view()
+ except TypeError as e:
+ self.assertEqual(str(e), "The `actions` argument must be provided "
+ "when calling `.as_view()` on a ViewSet. "
+ "For example `.as_view({'get': 'list'})`")
+ else:
+ self.fail("actions must not be empty.")
diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py
new file mode 100644
index 00000000..dd3bbd6e
--- /dev/null
+++ b/tests/test_write_only_fields.py
@@ -0,0 +1,31 @@
+from django.test import TestCase
+from rest_framework import serializers
+
+
+class WriteOnlyFieldTests(TestCase):
+ def setUp(self):
+ class ExampleSerializer(serializers.Serializer):
+ email = serializers.EmailField()
+ password = serializers.CharField(write_only=True)
+
+ def create(self, attrs):
+ return attrs
+
+ self.Serializer = ExampleSerializer
+
+ def write_only_fields_are_present_on_input(self):
+ data = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(data=data)
+ self.assertTrue(serializer.is_valid())
+ self.assertEquals(serializer.validated_data, data)
+
+ def write_only_fields_are_not_present_on_output(self):
+ instance = {
+ 'email': 'foo@example.com',
+ 'password': '123'
+ }
+ serializer = self.Serializer(instance)
+ self.assertEquals(serializer.data, {'email': 'foo@example.com'})
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 00000000..41f527df
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,6 @@
+"""
+Blank URLConf just to keep the test suite happy
+"""
+from django.conf.urls import patterns
+
+urlpatterns = patterns('')
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 00000000..b9034996
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,77 @@
+from django.core.exceptions import ObjectDoesNotExist
+from django.core.urlresolvers import NoReverseMatch
+
+
+class UsingURLPatterns(object):
+ """
+ Isolates URL patterns used during testing on the test class itself.
+ For example:
+
+ class MyTestCase(UsingURLPatterns, TestCase):
+ urlpatterns = [
+ ...
+ ]
+
+ def test_something(self):
+ ...
+ """
+ urls = __name__
+
+ def setUp(self):
+ global urlpatterns
+ urlpatterns = self.urlpatterns
+
+ def tearDown(self):
+ global urlpatterns
+ urlpatterns = []
+
+
+class MockObject(object):
+ def __init__(self, **kwargs):
+ self._kwargs = kwargs
+ for key, val in kwargs.items():
+ setattr(self, key, val)
+
+ def __str__(self):
+ kwargs_str = ', '.join([
+ '%s=%s' % (key, value)
+ for key, value in sorted(self._kwargs.items())
+ ])
+ return '<MockObject %s>' % kwargs_str
+
+
+class MockQueryset(object):
+ def __init__(self, iterable):
+ self.items = iterable
+
+ def get(self, **lookup):
+ for item in self.items:
+ if all([
+ getattr(item, key, None) == value
+ for key, value in lookup.items()
+ ]):
+ return item
+ raise ObjectDoesNotExist()
+
+
+class BadType(object):
+ """
+ When used as a lookup with a `MockQueryset`, these objects
+ will raise a `TypeError`, as occurs in Django when making
+ queryset lookups with an incorrect type for the lookup value.
+ """
+ def __eq__(self):
+ raise TypeError()
+
+
+def mock_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ args = args or []
+ kwargs = kwargs or {}
+ value = (args + list(kwargs.values()) + ['-'])[0]
+ prefix = 'http://example.org' if request else ''
+ suffix = ('.' + format) if (format is not None) else ''
+ return '%s/%s/%s%s/' % (prefix, view_name, value, suffix)
+
+
+def fail_reverse(view_name, args=None, kwargs=None, request=None, format=None):
+ raise NoReverseMatch()
diff --git a/tox.ini b/tox.ini
index aa97fd35..c986250c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,102 +1,31 @@
[tox]
-downloadcache = {toxworkdir}/cache/
-envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
+envlist =
+ py27-{flake8,docs},
+ {py26,py27}-django14,
+ {py26,py27,py32,py33,py34}-django{15,16},
+ {py27,py32,py33,py34}-django{17,18beta}
[testenv]
-commands = {envpython} rest_framework/runtests/runtests.py
-
-[testenv:py3.3-django1.6]
-basepython = python3.3
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py3.2-django1.6]
-basepython = python3.2
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py2.7-django1.6]
-basepython = python2.7
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
-
-[testenv:py2.6-django1.6]
-basepython = python2.6
-deps = https://www.djangoproject.com/download/1.6a1/tarball/
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
-
-[testenv:py3.3-django1.5]
-basepython = python3.3
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py3.2-django1.5]
-basepython = python3.2
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
-
-[testenv:py2.7-django1.5]
-basepython = python2.7
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.5]
-basepython = python2.6
-deps = django==1.5
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.7-django1.4]
-basepython = python2.7
-deps = django==1.4.3
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.4]
-basepython = python2.6
-deps = django==1.4.3
- django-filter==0.6a1
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.7-django1.3]
-basepython = python2.7
-deps = django==1.3.5
- django-filter==0.5.4
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
-
-[testenv:py2.6-django1.3]
-basepython = python2.6
-deps = django==1.3.5
- django-filter==0.5.4
- defusedxml==0.3
- django-oauth-plus==2.0
- oauth2==1.5.211
- django-oauth2-provider==0.2.3
+commands = ./runtests.py --fast
+setenv =
+ PYTHONDONTWRITEBYTECODE=1
+deps =
+ django14: Django==1.4.11 # Should track minimum supported
+ django15: Django==1.5.6 # Should track minimum supported
+ django16: Django==1.6.3 # Should track minimum supported
+ django17: Django==1.7.2 # Should track maximum supported
+ django18beta: https://www.djangoproject.com/download/1.8b1/tarball/
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-optionals.txt
+
+[testenv:py27-flake8]
+deps =
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-codestyle.txt
+commands = ./runtests.py --lintonly
+
+[testenv:py27-docs]
+deps =
+ -rrequirements/requirements-testing.txt
+ -rrequirements/requirements-documentation.txt
+commands = mkdocs build